From 210c5976d267d728b83d076d5f04a09eaf9a3da3 Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Mon, 3 Aug 2026 23:21:11 -0700 Subject: [PATCH 01/37] feat(acp): complete harness v2 hard cutover Rebuild the ACP v2 change set linearly on the current main branch while preserving the pull request effective tree. Signed-off-by: Sertac Ozercan --- .agents/skills/agent-sandbox-deploy/SKILL.md | 137 +- .../references/validate.md | 27 +- .../skills/agent-substrate-deploy/SKILL.md | 98 +- .../references/validate.md | 23 +- .dockerignore | 5 + .../workflows/agentruntime-external-e2e.yml | 100 - .github/workflows/gateway-e2e.yml | 1 - .github/workflows/helm-chart.yml | 122 +- .github/workflows/live-acp-release-gate.yml | 180 + .github/workflows/live-acp-runtime-e2e.yml | 130 + .github/workflows/release-pr.yml | 3 +- .github/workflows/release.yml | 78 +- .github/workflows/test-e2e.yml | 1 + .golangci.yml | 12 + AGENTS.md | 134 +- Dockerfile | 14 +- Makefile | 139 +- NOTICE.md | 8 +- README.md | 80 +- api/v1alpha1/agent_runtime_crd_types.go | 377 +- api/v1alpha1/agent_runtime_types_test.go | 252 +- api/v1alpha1/agent_types.go | 2 +- api/v1alpha1/branch_claim_types.go | 101 + api/v1alpha1/control_record_types.go | 117 + api/v1alpha1/controller_epoch_types.go | 83 + api/v1alpha1/execution_types.go | 8 +- api/v1alpha1/external_effect_types.go | 189 + api/v1alpha1/external_effect_types_test.go | 55 + api/v1alpha1/prompt_attempt_types.go | 146 + api/v1alpha1/publication_types.go | 313 + api/v1alpha1/runtime_pool_types.go | 537 + api/v1alpha1/runtime_pool_types_test.go | 183 + api/v1alpha1/runtime_session_control_types.go | 172 + api/v1alpha1/task_runtime_types.go | 432 + api/v1alpha1/task_runtime_types_test.go | 187 + .../task_session_ref_validation_test.go | 178 + api/v1alpha1/task_types.go | 208 +- api/v1alpha1/zz_generated.deepcopy.go | 2362 +++- charts/orka/README.md | 17 +- .../crds/agent-customresourcedefinition.yaml | 59 +- ...agentruntime-customresourcedefinition.yaml | 560 +- .../branchclaim-customresourcedefinition.yaml | 197 + ...trollerepoch-customresourcedefinition.yaml | 109 + ...ionworkspace-customresourcedefinition.yaml | 783 ++ ...rkspaceclass-customresourcedefinition.yaml | 333 + ...orkspacepool-customresourcedefinition.yaml | 224 + ...paceprovider-customresourcedefinition.yaml | 292 + ...ternaleffect-customresourcedefinition.yaml | 191 + ...olparameters-customresourcedefinition.yaml | 51 + ...oviderconfig-customresourcedefinition.yaml | 51 + ...accesspolicy-customresourcedefinition.yaml | 549 + ...romptattempt-customresourcedefinition.yaml | 264 + .../publication-customresourcedefinition.yaml | 523 + ...itorymonitor-customresourcedefinition.yaml | 226 +- .../runtimepool-customresourcedefinition.yaml | 638 + ...ssioncontrol-customresourcedefinition.yaml | 309 + .../crds/task-customresourcedefinition.yaml | 888 +- .../crds/tool-customresourcedefinition.yaml | 150 +- charts/orka/templates/NOTES.txt | 6 +- charts/orka/templates/_helpers.tpl | 90 +- .../orka/templates/acp-artifact-secret.yaml | 19 + charts/orka/templates/deployment.yaml | 131 +- .../templates/harness-wrapper-deployment.yaml | 110 - .../templates/harness-wrapper-secret.yaml | 19 - .../templates/provider-proxy-deployment.yaml | 82 + .../provider-proxy-networkpolicy.yaml | 36 + .../orka/templates/provider-proxy-secret.yaml | 31 + .../templates/provider-proxy-service.yaml | 15 + .../provider-proxy-serviceaccount.yaml | 10 + .../orka/templates/publisher-deployment.yaml | 113 + .../templates/publisher-networkpolicy.yaml | 40 + charts/orka/templates/publisher-pvc.yaml | 17 + charts/orka/templates/publisher-secret.yaml | 23 + .../orka/templates/publisher-service.yaml | 9 +- charts/orka/templates/rbac.yaml | 35 +- charts/orka/templates/runtime-namespace.yaml | 13 + .../scm-egress-proxy-deployment.yaml | 85 + .../scm-egress-proxy-networkpolicy.yaml | 63 + .../templates/scm-egress-proxy-secret.yaml | 21 + .../templates/scm-egress-proxy-service.yaml | 15 + .../scm-egress-proxy-serviceaccount.yaml | 10 + charts/orka/templates/serviceaccount.yaml | 23 +- charts/orka/templates/store-pvc.yaml | 2 +- .../vekil-ingress-networkpolicy.yaml | 24 + charts/orka/values.yaml | 171 +- cmd/build/helmify/README.md | 11 +- cmd/build/helmify/main_test.go | 212 + cmd/build/helmify/static/README.md | 17 +- cmd/build/helmify/static/templates/NOTES.txt | 6 +- .../helmify/static/templates/_helpers.tpl | 90 +- .../static/templates/acp-artifact-secret.yaml | 19 + .../helmify/static/templates/deployment.yaml | 183 +- .../templates/harness-wrapper-deployment.yaml | 110 - .../templates/harness-wrapper-secret.yaml | 19 - .../templates/provider-proxy-deployment.yaml | 82 + .../provider-proxy-networkpolicy.yaml | 36 + .../templates/provider-proxy-secret.yaml | 31 + .../templates/provider-proxy-service.yaml | 15 + .../provider-proxy-serviceaccount.yaml | 10 + .../templates/publisher-deployment.yaml | 113 + .../templates/publisher-networkpolicy.yaml | 40 + .../static/templates/publisher-pvc.yaml | 17 + .../static/templates/publisher-secret.yaml | 23 + .../static/templates/publisher-service.yaml | 9 +- cmd/build/helmify/static/templates/rbac.yaml | 100 +- .../static/templates/runtime-namespace.yaml | 13 + .../scm-egress-proxy-deployment.yaml | 85 + .../scm-egress-proxy-networkpolicy.yaml | 63 + .../templates/scm-egress-proxy-secret.yaml | 21 + .../templates/scm-egress-proxy-service.yaml | 15 + .../scm-egress-proxy-serviceaccount.yaml | 10 + .../static/templates/serviceaccount.yaml | 23 +- .../helmify/static/templates/store-pvc.yaml | 2 +- .../vekil-ingress-networkpolicy.yaml | 24 + .../templates/workspace-class-use-policy.yaml | 72 - .../workspace-class-use-webhook.yaml | 66 - .../workspace-core-admission-policy.yaml | 44 - .../workspace-crd-upgrade-guard.yaml | 19 - cmd/build/helmify/static/values.yaml | 203 +- cmd/cli/helpers_test.go | 2 +- cmd/cli/main.go | 2 + cmd/cli/misc_commands.go | 54 +- cmd/cli/resource_commands.go | 37 +- cmd/cli/runtime.go | 132 + cmd/cli/runtime_test.go | 91 + cmd/cli/task.go | 24 +- cmd/cli/task_test.go | 10 +- cmd/cli/task_workspace.go | 269 + cmd/cli/task_workspace_test.go | 249 + cmd/main.go | 564 +- cmd/main_test.go | 457 + cmd/orka-acp-exec-helper/main.go | 15 + cmd/orka-acp-runtime/main.go | 80 + cmd/orka-agent-harness-wrapper/.gitignore | 4 - cmd/orka-agent-harness-wrapper/main.go | 140 - cmd/orka-image-ref-validator/main.go | 83 + cmd/orka-image-ref-validator/main_test.go | 37 + cmd/orka-provider-auth-proxy/credentials.go | 436 + .../credentials_test.go | 552 + cmd/orka-provider-auth-proxy/main.go | 136 + cmd/orka-provider-auth-proxy/proxy.go | 420 + cmd/orka-provider-auth-proxy/proxy_test.go | 159 + cmd/orka-scm-egress-proxy/auth.go | 89 + cmd/orka-scm-egress-proxy/config.go | 235 + cmd/orka-scm-egress-proxy/main.go | 201 + cmd/orka-scm-egress-proxy/network.go | 162 + cmd/orka-scm-egress-proxy/proxy.go | 421 + cmd/orka-scm-egress-proxy/proxy_test.go | 411 + cmd/orka-workspace-agent/main_test.go | 40 +- cmd/orka-workspace-publisher/main.go | 49 + cmd/orka-workspace-publisher/umask_other.go | 5 + cmd/orka-workspace-publisher/umask_unix.go | 9 + config/acp-production/README.md | 17 + config/acp-production/kustomization.yaml | 25 + config/acp-production/runtime-images.env | 6 + config/acp-workload/README.md | 16 + .../api_service.yaml} | 14 +- config/acp-workload/kustomization.yaml | 111 + config/acp-workload/kustomizeconfig.yaml | 9 + .../acp-workload/manager_metrics_patch.yaml | 4 + config/acp-workload/metrics_service.yaml | 18 + ...ve_admission_policy_binding_namespace.yaml | 12 + .../remove_admission_policy_namespace.yaml | 12 + .../crd/bases/core.orka.ai_agentruntimes.yaml | 560 +- config/crd/bases/core.orka.ai_agents.yaml | 15 +- .../crd/bases/core.orka.ai_branchclaims.yaml | 197 + .../bases/core.orka.ai_controllerepochs.yaml | 109 + .../bases/core.orka.ai_externaleffects.yaml | 191 + .../bases/core.orka.ai_promptattempts.yaml | 264 + .../crd/bases/core.orka.ai_publications.yaml | 529 + .../crd/bases/core.orka.ai_runtimepools.yaml | 638 + .../core.orka.ai_runtimesessioncontrols.yaml | 309 + config/crd/bases/core.orka.ai_tasks.yaml | 772 +- config/crd/kustomization.yaml | 7 + config/default/kustomization.yaml | 5 +- config/harness-wrapper/README.md | 22 - config/harness-wrapper/deployment.yaml | 110 - config/harness-wrapper/kustomization.yaml | 10 - config/harness-wrapper/serviceaccount.yaml | 8 - config/harness-wrapper/volume-mode-patch.yaml | 14 - config/manager/kustomization.yaml | 1 + config/manager/manager.yaml | 81 +- config/provider-proxy/deployment.yaml | 99 + config/provider-proxy/kustomization.yaml | 11 + config/provider-proxy/networkpolicy.yaml | 44 + config/provider-proxy/service.yaml | 15 + config/provider-proxy/serviceaccount.yaml | 8 + config/publisher/deployment.yaml | 133 + config/publisher/kustomization.yaml | 10 + config/publisher/networkpolicy.yaml | 46 + config/publisher/pvc.yaml | 13 + config/publisher/service.yaml | 15 + config/publisher/serviceaccount.yaml | 8 + config/rbac/role.yaml | 46 +- config/samples/core_v1alpha1_agent.yaml | 15 +- .../samples/core_v1alpha1_agent_claude.yaml | 23 +- config/samples/core_v1alpha1_agent_codex.yaml | 7 +- .../samples/core_v1alpha1_agent_opencode.yaml | 19 - .../samples/core_v1alpha1_agentruntime.yaml | 77 +- .../core_v1alpha1_agentruntime_agentkit.yaml | 26 - .../core_v1alpha1_agentruntime_foundry.yaml | 26 - config/samples/core_v1alpha1_task_agent.yaml | 36 +- .../core_v1alpha1_task_agent_copilot.yaml | 21 - .../core_v1alpha1_task_agent_workspace.yaml | 46 +- config/samples/kustomization.yaml | 5 +- config/scm-egress-proxy/README.md | 21 + config/scm-egress-proxy/deployment.yaml | 91 + config/scm-egress-proxy/kustomization.yaml | 11 + config/scm-egress-proxy/networkpolicy.yaml | 70 + config/scm-egress-proxy/service.yaml | 15 + config/scm-egress-proxy/serviceaccount.yaml | 8 + config/vekil-ingress/kustomization.yaml | 4 + config/vekil-ingress/networkpolicy.yaml | 23 + deploy/orka.yaml | 8246 ++---------- ...01-execution-workspace-default-provider.md | 10 +- ...ider-neutral-execution-workspace-status.md | 14 +- ...use-provider-route-for-workspace-daemon.md | 10 +- ...04-use-minimal-substrate-control-client.md | 8 +- ...05-require-explicit-substrate-api-trust.md | 10 +- ...per-first-execution-workspace-providers.md | 14 +- ...7-substrate-actor-pool-oversubscription.md | 12 +- ...08-runtime-session-internal-store-first.md | 34 +- ...-agentkit-byoa-harness-runtime-boundary.md | 51 - ...fer-runtime-session-ui-until-public-api.md | 36 +- docs/development/acp-crd-hard-cutover.md | 207 + docs/development/execution-events-wave-0.md | 2 +- docs/development/execution-events-wave-4.md | 2 +- ...-workspace-provider-implementation-plan.md | 2 +- docs/development/harness-conformance.md | 90 - docs/development/harness-protocol-mvp.md | 157 - .../post-p0-evented-runtime-checklist.md | 4 +- .../remaining-frontier-readiness.md | 28 +- ...y-security-scanning-implementation-plan.md | 4 +- docs/pr-monitor-clawsweeper-parity-plan.md | 10 +- .../README.md | 105 - .../SECURITY.md | 29 - examples/fibey-custom-agent-demo/README.md | 133 - .../agent-agentkit.yaml | 12 - .../agent-foundry.yaml | 12 - examples/fibey-custom-agent-demo/agent.yaml | 11 - .../agentkit-runtime-offline.example.yaml | 50 - .../agentruntime-agentkit.yaml | 23 - .../agentruntime-foundry.yaml | 22 - .../fibey-custom-agent-demo/agentruntime.yaml | 18 - .../kustomization.yaml | 6 - .../mock-http-runtime-service.yaml | 50 - .../secret-agentkit.yaml | 13 - .../secret-foundry.yaml | 13 - examples/fibey-custom-agent-demo/secret.yaml | 11 - .../fibey-custom-agent-demo/switch-backend.sh | 48 - examples/fibey-custom-agent-demo/task.yaml | 11 - examples/github-cicd/README.md | 69 +- examples/github-cicd/agents.yaml | 54 +- .../github-cicd/github-actions-webhook.yaml | 47 +- examples/github-cicd/secret.yaml | 15 +- examples/github-cicd/task.yaml | 8 +- examples/github-label-trigger/README.md | 8 +- examples/harness/echo/Dockerfile | 13 - examples/harness/echo/main.go | 638 - examples/harness/echo/main_test.go | 250 - examples/iterative-review/coder-agent.yaml | 2 + .../iterative-review/coordinator-agent.yaml | 22 +- examples/iterative-review/iterative-task.yaml | 5 +- examples/iterative-review/reviewer-agent.yaml | 2 +- .../support-escalation-runtime-demo/README.md | 45 - .../agent.yaml | 11 - .../agentruntime.yaml | 22 - .../kustomization.yaml | 7 - .../mock-http-runtime-service.yaml | 55 - .../mock-support-tool-service.yaml | 45 - .../support-escalation-runtime-demo/task.yaml | 14 - .../tools.yaml | 20 - go.mod | 8 +- go.sum | 8 +- hack/agent-substrate/README.md | 107 + ...tenet-router-authorization-redaction.patch | 311 + .../ateom-runsc-delete-recovery.patch | 2018 +++ hack/demos/README.md | 29 +- hack/demos/RECORDING.md | 85 +- hack/demos/cluster/cluster-down.sh | 6 + hack/demos/cluster/cluster-up.sh | 40 +- hack/demos/cluster/install-demo-model.sh | 69 +- hack/demos/cluster/install-substrate.sh | 285 +- .../cluster/templates/orka-live-template.yaml | 12 +- hack/demos/images/sandbox-runtime/Dockerfile | 14 +- hack/demos/lib/manifests.sh | 56 +- internal/acp/client.go | 422 + internal/acp/client_test.go | 290 + internal/acp/pins.go | 33 + internal/acp/process.go | 440 + internal/acp/process_linux.go | 380 + internal/acp/process_linux_test.go | 746 ++ internal/acp/process_test.go | 132 + .../acp/process_test_support_other_test.go | 21 + internal/acp/process_unix.go | 51 + internal/acp/process_windows.go | 35 + internal/acp/protocol.go | 276 + internal/acp/runtime_env.go | 394 + internal/acp/runtime_env_test.go | 345 + internal/acp/session.go | 677 + internal/acp/session_test.go | 448 + internal/acp/supervisor_hardening.go | 17 + internal/acp/supervisor_hardening_linux.go | 16 + internal/acp/supervisor_hardening_nonunix.go | 9 + internal/acp/supervisor_hardening_unix.go | 13 + internal/api/acp_artifact_authorization.go | 392 + .../api/acp_artifact_authorization_test.go | 636 + internal/api/acp_artifact_handlers.go | 198 + internal/api/acp_artifact_handlers_test.go | 298 + internal/api/acp_credential_broker.go | 322 + ...acp_credential_broker_continuation_test.go | 202 + internal/api/acp_credential_broker_test.go | 270 + internal/api/acp_mcp_broker_registration.go | 34 + .../api/acp_mcp_broker_registration_test.go | 58 + internal/api/anthropic_tool_loop.go | 4 +- internal/api/artifact_handlers_test.go | 262 - internal/api/chat.go | 373 +- internal/api/chat_system_prompt.go | 2 +- internal/api/chat_test.go | 169 +- internal/api/chat_tool_executor_test.go | 31 +- internal/api/context_token_authorization.go | 48 +- .../api/context_token_authorization_test.go | 55 +- internal/api/github_label_webhook.go | 19 +- internal/api/github_label_webhook_test.go | 44 +- internal/api/handlers.go | 14 +- internal/api/handlers_test.go | 60 +- internal/api/internal_caller_authorization.go | 112 +- internal/api/internal_handlers.go | 10 - internal/api/resource_handlers.go | 71 +- internal/api/resource_handlers_test.go | 164 + internal/api/runtime_resource_handlers.go | 243 + .../api/runtime_resource_handlers_test.go | 187 + internal/api/security_handlers.go | 37 +- internal/api/security_handlers_test.go | 23 +- internal/api/server.go | 25 +- internal/api/static.go | 2 + internal/api/task_workspace.go | 49 + internal/api/task_workspace_test.go | 30 + internal/artifactcap/capability.go | 121 + internal/artifactcap/capability_test.go | 97 + internal/artifactcap/digest.go | 60 + internal/artifactcap/io_helpers.go | 82 + internal/artifactcap/ledger.go | 230 + internal/artifactcap/nofollow_other.go | 14 + internal/artifactcap/nofollow_unix.go | 41 + internal/artifactcap/retention.go | 853 ++ internal/artifactcap/retention_test.go | 436 + internal/artifactcap/root_lock_gate.go | 34 + internal/artifactcap/root_lock_other.go | 9 + internal/artifactcap/root_lock_unix.go | 392 + internal/artifactcap/service.go | 262 + internal/artifactcap/service_test.go | 296 + internal/artifactcap/store.go | 329 + internal/artifactcap/types.go | 254 + .../controller/acp_agent_configuration.go | 235 + .../acp_agent_configuration_skills_test.go | 152 + .../acp_agent_configuration_test.go | 314 + .../acp_agent_configuration_tools_test.go | 185 + .../controller/acp_artifact_retention_test.go | 282 + internal/controller/acp_credentials.go | 184 + internal/controller/acp_dispatcher.go | 3716 ++++++ ...spatcher_prompt_attempt_settlement_test.go | 451 + internal/controller/acp_dispatcher_session.go | 785 ++ .../acp_dispatcher_session_bootstrap_test.go | 207 + internal/controller/acp_dispatcher_test.go | 2937 +++++ internal/controller/acp_external_effect.go | 285 + internal/controller/acp_frozen_authority.go | 71 + .../acp_frozen_authority_settlement_test.go | 283 + internal/controller/acp_idle_pool_test.go | 315 + internal/controller/acp_mcp_broker.go | 664 + ...p_broker_coordination_registration_test.go | 65 + internal/controller/acp_mcp_broker_test.go | 605 + internal/controller/acp_mcp_policy.go | 332 + internal/controller/acp_mcp_policy_test.go | 318 + internal/controller/acp_outbox.go | 313 + internal/controller/acp_outbox_test.go | 95 + internal/controller/acp_publication.go | 1453 +++ .../acp_publication_receipt_test.go | 116 + .../acp_publication_reclamation_test.go | 590 + .../controller/acp_publication_recovery.go | 483 + internal/controller/acp_recovery.go | 799 ++ internal/controller/acp_recovery_test.go | 1346 ++ .../acp_runtime_pool_reserved_rebind_test.go | 243 + .../acp_runtime_pool_rotation_rebind_test.go | 319 + internal/controller/acp_runtime_profile.go | 283 + .../controller/acp_runtime_profile_test.go | 324 + internal/controller/acp_session.go | 737 ++ internal/controller/acp_session_json.go | 23 + internal/controller/acp_session_recovery.go | 182 + .../controller/acp_session_recovery_test.go | 133 + internal/controller/acp_session_runtime.go | 168 + internal/controller/acp_session_test.go | 790 ++ internal/controller/acp_session_transcript.go | 303 + internal/controller/acp_task_finalizer.go | 528 + .../controller/acp_task_finalizer_test.go | 364 + internal/controller/acp_task_queue.go | 1127 ++ .../acp_task_queue_reconciliation_test.go | 394 + internal/controller/acp_task_queue_test.go | 783 ++ internal/controller/acp_upgrade_drain.go | 1402 +++ internal/controller/acp_upgrade_drain_test.go | 834 ++ internal/controller/acp_workspace.go | 326 + ...rkspace_repository_identity_casing_test.go | 103 + internal/controller/acp_workspace_test.go | 186 + internal/controller/agent_controller.go | 29 +- .../controller/agent_controller_unit_test.go | 137 + internal/controller/agent_execution_plan.go | 89 +- .../controller/agent_execution_plan_test.go | 166 +- ...ecution_plan_unsupported_semantics_test.go | 105 + .../controller/agent_runtime_controller.go | 667 +- .../agent_runtime_controller_test.go | 1079 +- internal/controller/controller_epoch.go | 123 + internal/controller/controller_epoch_test.go | 91 + .../controller/execution_workspace_config.go | 4 +- .../execution_workspace_controller.go | 2 +- .../fake_workspace_provider_controller.go | 2 +- internal/controller/harness_broker.go | 1339 -- internal/controller/harness_wrapper.go | 2664 ---- internal/controller/harness_wrapper_test.go | 3609 ------ internal/controller/job_builder.go | 147 +- internal/controller/job_builder_test.go | 302 +- .../outboundaccesspolicy_controller_test.go | 179 - .../repositorymonitor_controller.go | 2 +- .../repositorymonitor_controller_test.go | 191 +- .../controller/repositorymonitor_inventory.go | 13 +- .../repositorymonitor_issue_actions.go | 150 +- .../controller/repositorymonitor_repair.go | 123 +- .../controller/repositoryscan_controller.go | 48 +- .../repositoryscan_controller_test.go | 6 +- .../controller/runtime_pool_controller.go | 2877 +++++ .../runtime_pool_controller_test.go | 1703 +++ .../controller/session_cleanup_recovery.go | 60 + .../session_cleanup_recovery_test.go | 61 + internal/controller/session_manager.go | 36 +- internal/controller/session_manager_test.go | 25 +- internal/controller/task_controller.go | 396 +- internal/controller/task_controller_test.go | 2 +- .../controller/task_controller_unit_test.go | 802 +- .../controller/task_workspace_credentials.go | 42 + .../workspace_api_validation_test.go | 108 +- internal/harness/client.go | 1230 -- internal/harness/client_test.go | 1583 --- .../conformance/agentkit_external_test.go | 357 - internal/harness/conformance/conformance.go | 1535 --- .../harness/conformance/conformance_test.go | 1896 --- internal/harness/frame_identity.go | 94 - internal/harness/frame_identity_test.go | 66 - internal/harness/harnesstest/conformance.go | 275 - internal/harness/harnesstest/fake_server.go | 396 - .../harness/harnesstest/fake_server_test.go | 41 - internal/harness/lifecycle.go | 187 - internal/harness/lifecycle_test.go | 64 - internal/harness/mapper.go | 154 - internal/harness/mapper_test.go | 123 - internal/harness/protocol.go | 708 -- internal/harness/protocol_test.go | 360 - internal/harness/runner.go | 133 - internal/harness/runner_test.go | 314 - internal/harness/runtime_session_identity.go | 79 - .../harness/runtime_session_identity_test.go | 89 - internal/harness/runtime_session_store.go | 52 - internal/harness/transport.go | 43 - internal/harness/turn_journal.go | 167 - internal/harness/turn_journal_test.go | 198 - internal/harness/turn_path.go | 93 - internal/harness/turn_path_test.go | 70 - internal/harness/v2/capability.go | 126 + internal/harness/v2/capability_test.go | 49 + internal/harness/v2/classification.go | 215 + .../v2/classification_lifecycle_test.go | 205 + internal/harness/v2/client.go | 938 ++ internal/harness/v2/client_errors.go | 159 + internal/harness/v2/client_stream.go | 319 + internal/harness/v2/client_stream_test.go | 169 + internal/harness/v2/client_test.go | 667 + internal/harness/v2/conformance/check.go | 426 + internal/harness/v2/conformance/check_test.go | 290 + .../v2/conformance/conformancetest/server.go | 928 ++ internal/harness/v2/conformance/cycle.go | 656 + internal/harness/v2/conformance/doc.go | 5 + internal/harness/v2/conformance/replay.go | 402 + internal/harness/v2/conformance/types.go | 59 + internal/harness/v2/digest.go | 430 + internal/harness/v2/doc.go | 7 + internal/harness/v2/event_types.go | 544 + internal/harness/v2/identity.go | 215 + internal/harness/v2/lifecycle.go | 173 + internal/harness/v2/mcp.go | 452 + internal/harness/v2/mcp_test.go | 164 + internal/harness/v2/ndjson.go | 369 + internal/harness/v2/ndjson_test.go | 291 + internal/harness/v2/paths.go | 122 + internal/harness/v2/paths_digest_test.go | 269 + internal/harness/v2/prompt.go | 600 + internal/harness/v2/protocol.go | 588 + internal/harness/v2/semantics.go | 109 + internal/harness/v2/session.go | 726 ++ .../v2/session_prompt_workspace_test.go | 570 + internal/harness/v2/test_helpers_test.go | 211 + internal/harness/v2/validation.go | 70 + internal/harness/v2/workspace.go | 224 + internal/publisher/artifact.go | 386 + internal/publisher/command_other.go | 12 + internal/publisher/command_unix.go | 20 + internal/publisher/doc.go | 9 + internal/publisher/errors.go | 76 + internal/publisher/git.go | 360 + internal/publisher/git_test.go | 97 + internal/publisher/pr.go | 79 + internal/publisher/prepare.go | 451 + internal/publisher/proxy_environment.go | 133 + internal/publisher/proxy_environment_test.go | 76 + internal/publisher/publish.go | 298 + internal/publisher/publisher.go | 219 + internal/publisher/publisher_test.go | 1237 ++ internal/publisher/reclaim.go | 129 + .../service/artifact_authorization.go | 186 + .../service/artifact_authorization_test.go | 97 + internal/publisher/service/artifact_client.go | 181 + internal/publisher/service/capability.go | 171 + internal/publisher/service/capability_test.go | 32 + internal/publisher/service/client.go | 218 + internal/publisher/service/config.go | 219 + internal/publisher/service/credential.go | 174 + .../publisher/service/credential_broker.go | 151 + .../service/credential_broker_test.go | 66 + .../service/development_fallback_test.go | 79 + internal/publisher/service/doc.go | 13 + internal/publisher/service/env.go | 309 + internal/publisher/service/env_test.go | 31 + internal/publisher/service/errors.go | 73 + internal/publisher/service/git.go | 678 + .../publisher/service/git_workspace_test.go | 289 + internal/publisher/service/github_pr.go | 697 ++ internal/publisher/service/github_pr_test.go | 680 + internal/publisher/service/handlers.go | 570 + internal/publisher/service/journal.go | 683 + internal/publisher/service/journal_test.go | 211 + internal/publisher/service/process_other.go | 12 + internal/publisher/service/process_unix.go | 20 + .../service/proxy_environment_test.go | 47 + internal/publisher/service/server.go | 403 + internal/publisher/service/service_test.go | 1209 ++ internal/publisher/service/types.go | 347 + internal/publisher/service/validation.go | 282 + internal/publisher/service/workspace.go | 245 + internal/publisher/storage.go | 479 + internal/publisher/types.go | 229 + internal/publisher/validation.go | 192 + internal/publisher/validation_test.go | 60 + internal/publisher/verify.go | 194 + internal/safesymlink/symlink.go | 128 + internal/safesymlink/symlink_test.go | 38 + internal/store/control_store.go | 88 + internal/store/control_types.go | 1301 ++ internal/store/control_types_test.go | 123 + internal/store/kube/branch_claim.go | 470 + .../kube/branch_claim_reclamation_test.go | 154 + internal/store/kube/controller_epoch.go | 379 + internal/store/kube/external_effect.go | 315 + internal/store/kube/helpers.go | 227 + internal/store/kube/outbox.go | 59 + internal/store/kube/prompt_attempt.go | 1444 +++ .../store/kube/prompt_attempt_reclaim_test.go | 531 + internal/store/kube/publication.go | 861 ++ internal/store/kube/session_cleanup.go | 460 + internal/store/kube/session_cleanup_test.go | 437 + internal/store/kube/session_control.go | 1035 ++ internal/store/kube/session_turn.go | 726 ++ internal/store/kube/store.go | 175 + internal/store/kube/store_test.go | 1588 +++ internal/store/outbox_persistence.go | 13 + internal/store/session_cleanup.go | 102 + internal/store/session_turn_persistence.go | 60 + .../sqlite/branch_claim_reclamation_test.go | 119 + internal/store/sqlite/branch_claim_store.go | 448 + internal/store/sqlite/control_helpers.go | 152 + internal/store/sqlite/control_migrations.go | 448 + internal/store/sqlite/control_store_test.go | 907 ++ .../store/sqlite/controller_epoch_store.go | 143 + .../store/sqlite/external_effect_store.go | 288 + internal/store/sqlite/message_store.go | 14 +- internal/store/sqlite/message_store_test.go | 44 + internal/store/sqlite/outbox_store.go | 493 + internal/store/sqlite/prompt_attempt_store.go | 1199 ++ internal/store/sqlite/publication_store.go | 841 ++ .../store/sqlite/runtime_session_store.go | 362 - .../sqlite/runtime_session_store_test.go | 533 - internal/store/sqlite/session_cleanup.go | 637 + internal/store/sqlite/session_cleanup_test.go | 134 + .../store/sqlite/session_control_store.go | 1203 ++ .../sqlite/session_delete_control_test.go | 189 + internal/store/sqlite/session_store.go | 349 +- .../store/sqlite/session_turn_persistence.go | 460 + .../sqlite/session_turn_persistence_test.go | 101 + internal/store/sqlite/sqlite.go | 38 +- internal/store/store.go | 13 + internal/tools/auto_merge_pull_request.go | 25 +- .../tools/auto_merge_pull_request_test.go | 141 +- .../broker_coordination_registration_test.go | 460 + internal/tools/chat_create_agent.go | 84 +- internal/tools/chat_create_agent_test.go | 502 +- internal/tools/check_pr_review_marker.go | 2 +- internal/tools/check_pr_review_marker_test.go | 1 + internal/tools/check_pull_request_ci.go | 2 +- internal/tools/check_pull_request_ci_test.go | 10 +- internal/tools/comment_on_issue.go | 2 +- internal/tools/comment_on_issue_test.go | 7 +- internal/tools/create_agent.go | 50 +- internal/tools/create_agent_task.go | 94 +- internal/tools/create_agent_task_test.go | 155 +- internal/tools/create_agent_test.go | 368 +- internal/tools/create_container_task.go | 38 +- internal/tools/create_container_task_test.go | 44 +- internal/tools/create_pr_monitor.go | 22 +- internal/tools/create_pr_monitor_test.go | 68 +- internal/tools/create_pull_request.go | 2 +- internal/tools/create_pull_request_test.go | 60 +- internal/tools/delegate_task.go | 453 +- internal/tools/delegate_task_test.go | 100 +- internal/tools/file_read.go | 112 +- internal/tools/file_read_test.go | 19 +- internal/tools/get_issue.go | 2 +- internal/tools/get_issue_test.go | 7 +- internal/tools/github_helpers.go | 151 +- internal/tools/github_helpers_test.go | 344 +- internal/tools/integration_test.go | 13 +- internal/tools/list_issues.go | 2 +- internal/tools/list_issues_test.go | 9 +- internal/tools/list_pull_requests.go | 2 +- internal/tools/list_pull_requests_test.go | 13 +- internal/tools/memory_tools.go | 126 +- internal/tools/merge_pull_request.go | 64 +- internal/tools/merge_pull_request_test.go | 139 +- internal/tools/post_review_comment.go | 2 +- internal/tools/post_review_comment_test.go | 74 +- internal/tools/registry.go | 52 + internal/tools/review_pull_request.go | 2 +- internal/tools/review_pull_request_test.go | 102 +- internal/tools/secret_resolution.go | 68 +- internal/tools/send_message.go | 27 +- internal/tools/send_message_test.go | 102 + internal/tools/test_helpers_test.go | 9 +- .../transaction_context_authorization.go | 25 +- .../transaction_context_authorization_test.go | 57 + internal/tools/wait_for_tasks.go | 42 +- internal/tools/web_fetch.go | 57 +- internal/tools/web_fetch_test.go | 12 +- internal/workspacedelta/archive.go | 130 + internal/workspacedelta/build_limits_test.go | 52 + internal/workspacedelta/compare.go | 250 + internal/workspacedelta/doc.go | 7 + internal/workspacedelta/errors.go | 47 + internal/workspacedelta/nofollow_unix.go | 37 + .../workspacedelta/nofollow_unsupported.go | 15 + internal/workspacedelta/path.go | 159 + internal/workspacedelta/policy.go | 224 + internal/workspacedelta/security_unix_test.go | 75 + internal/workspacedelta/snapshot.go | 302 + internal/workspacedelta/types.go | 160 + .../workspacedelta/workspacedelta_test.go | 391 + manifest_staging/charts/orka/README.md | 17 +- .../crds/agent-customresourcedefinition.yaml | 15 +- ...agentruntime-customresourcedefinition.yaml | 560 +- .../branchclaim-customresourcedefinition.yaml | 197 + ...trollerepoch-customresourcedefinition.yaml | 109 + ...ternaleffect-customresourcedefinition.yaml | 191 + ...romptattempt-customresourcedefinition.yaml | 264 + .../publication-customresourcedefinition.yaml | 529 + .../runtimepool-customresourcedefinition.yaml | 638 + ...ssioncontrol-customresourcedefinition.yaml | 309 + .../crds/task-customresourcedefinition.yaml | 772 +- .../charts/orka/templates/NOTES.txt | 6 +- .../charts/orka/templates/_helpers.tpl | 90 +- .../orka/templates/acp-artifact-secret.yaml | 19 + .../charts/orka/templates/deployment.yaml | 183 +- .../templates/harness-wrapper-deployment.yaml | 110 - .../templates/harness-wrapper-secret.yaml | 19 - .../templates/provider-proxy-deployment.yaml | 82 + .../provider-proxy-networkpolicy.yaml | 36 + .../orka/templates/provider-proxy-secret.yaml | 31 + .../templates/provider-proxy-service.yaml | 15 + .../provider-proxy-serviceaccount.yaml | 10 + .../orka/templates/publisher-deployment.yaml | 113 + .../templates/publisher-networkpolicy.yaml | 40 + .../charts/orka/templates/publisher-pvc.yaml | 17 + .../orka/templates/publisher-secret.yaml | 23 + .../orka/templates/publisher-service.yaml | 9 +- .../charts/orka/templates/rbac.yaml | 100 +- .../orka/templates/runtime-namespace.yaml | 13 + .../scm-egress-proxy-deployment.yaml | 85 + .../scm-egress-proxy-networkpolicy.yaml | 63 + .../templates/scm-egress-proxy-secret.yaml | 21 + .../templates/scm-egress-proxy-service.yaml | 15 + .../scm-egress-proxy-serviceaccount.yaml | 10 + .../charts/orka/templates/serviceaccount.yaml | 23 +- .../charts/orka/templates/store-pvc.yaml | 2 +- .../vekil-ingress-networkpolicy.yaml | 24 + .../templates/workspace-class-use-policy.yaml | 72 - .../workspace-class-use-webhook.yaml | 66 - .../workspace-core-admission-policy.yaml | 44 - .../workspace-crd-upgrade-guard.yaml | 19 - manifest_staging/charts/orka/values.yaml | 203 +- manifest_staging/deploy/orka.yaml | 10454 ++-------------- pkg/harness/conformance/conformance.go | 20 - pkg/harness/conformance/doc.go | 3 - pkg/harness/doc.go | 7 - pkg/harness/harness.go | 190 - pkg/harness/harness_test.go | 31 - scripts/agent-substrate-e2e.sh | 1129 +- scripts/apply-acp-production.sh | 57 + scripts/check-legacy-wrapper-resources.sh | 28 + scripts/lib/kind-local-registry.sh | 83 + .../lib/live-acp-runtime-kind-bootstrap.sh | 453 + scripts/live-acp-runtime-e2e.sh | 3316 +++++ scripts/live-acp-runtime-kind-e2e.sh | 142 + scripts/live-agent-sandbox-e2e.sh | 332 +- scripts/live-copilot-proxy-e2e.sh | 122 +- scripts/live-github-label-trigger-e2e.sh | 31 +- scripts/live-github-oidc-e2e.sh | 35 +- scripts/render-acp-runtime-images.sh | 47 + scripts/render-worker-images.sh | 36 + scripts/security-scan-e2e.sh | 546 +- scripts/sync-helm-crds.sh | 293 + .../tests/acp-runtime-image-rollout-test.sh | 238 + scripts/tests/acp-secret-mounts-test.sh | 82 + .../agent-substrate-e2e-hardening-test.sh | 275 + scripts/tests/agent-substrate-patches-test.sh | 326 + scripts/tests/live-acp-runtime-e2e-test.sh | 687 + .../tests/live-acp-runtime-kind-e2e-test.sh | 298 + ...e-acp-runtime-port-forward-cleanup-test.sh | 133 + scripts/tests/sync-helm-crds-test.sh | 299 + scripts/tests/upgrade-orka-crds-test.sh | 654 + scripts/update-release-version.py | 3 - scripts/upgrade-orka-crds.sh | 664 + test/e2e/agent_advanced_test.go | 30 +- test/e2e/agent_claude_test.go | 179 +- test/e2e/agent_copilot_test.go | 178 +- test/e2e/agent_session_test.go | 47 +- test/e2e/agent_test.go | 17 +- test/e2e/agent_ttl_test.go | 1 + test/e2e/agent_workspace_test.go | 26 +- .../agentruntime_brokered_external_test.go | 235 +- test/e2e/agentruntime_external_test.go | 385 +- test/e2e/comprehensive_test.go | 138 +- test/e2e/e2e_suite_test.go | 260 +- test/e2e/gateway_test.go | 54 +- test/e2e/helpers_test.go | 276 +- ...agent_runtime_matrix_manifest_unit_test.go | 74 + test/e2e/live_agent_runtime_matrix_test.go | 399 +- test/e2e/negative_test.go | 44 +- test/e2e/pr_workflow_test.go | 485 +- test/e2e/sqlite_storage_test.go | 14 +- test/e2e/tools_test.go | 38 +- test/e2e/workspace_advanced_test.go | 656 +- .../agents/agent-create-form.test.tsx | 301 +- .../components/agents/agent-create-form.tsx | 189 +- .../components/agents/agent-detail.test.tsx | 12 +- ui/src/components/agents/agent-detail.tsx | 22 +- ui/src/components/agents/agent-list.test.tsx | 4 +- ui/src/components/agents/agent-list.tsx | 6 +- ui/src/components/layout/sidebar.test.tsx | 1 + ui/src/components/layout/sidebar.tsx | 4 +- .../repository-monitor-create-form.tsx | 2 +- .../runtime/runtime-registry.test.tsx | 105 + .../components/runtime/runtime-registry.tsx | 227 + ui/src/components/runtime/task-flow-panel.tsx | 2 +- ui/src/components/tasks/fork-dialog.tsx | 2 +- .../tasks/task-create-form.test.tsx | 279 +- ui/src/components/tasks/task-create-form.tsx | 390 +- ui/src/components/tasks/task-detail.tsx | 8 - .../tasks/task-execution-panel.test.tsx | 63 + .../components/tasks/task-execution-panel.tsx | 286 +- ui/src/hooks/use-runtimes.test.tsx | 223 + ui/src/hooks/use-runtimes.ts | 61 + ui/src/routeTree.gen.ts | 21 + ui/src/routes/runtimes/index.tsx | 6 + ui/src/schemas/agent.test.ts | 180 +- ui/src/schemas/agent.ts | 23 +- ui/src/schemas/runtime.test.ts | 118 + ui/src/schemas/runtime.ts | 206 + ui/src/schemas/task.test.ts | 96 +- ui/src/schemas/task.ts | 137 +- website/docs/concepts/agent-runtimes.md | 839 +- website/docs/concepts/agent-sandbox.md | 223 +- website/docs/concepts/architecture.md | 155 +- website/docs/concepts/configuration.md | 219 +- website/docs/concepts/memory.md | 4 +- website/docs/concepts/security.md | 105 +- website/docs/concepts/substrate.md | 639 +- .../agent-runtime-adapter-contract.md | 135 +- website/docs/development/development.md | 51 +- .../development/security-scanning-design.md | 43 +- website/docs/development/testing.md | 164 +- website/docs/getting-started.md | 123 +- .../guides/bring-your-own-agent-runtime.md | 224 +- website/docs/guides/chat.md | 2 +- website/docs/guides/cli-harness-wrapper.md | 141 - website/docs/guides/github-label-triggers.md | 16 +- .../docs/guides/multi-agent-coordination.md | 65 +- website/docs/guides/observability.md | 38 +- website/docs/guides/repository-monitors.md | 16 +- .../guides/repository-security-scanning.md | 11 +- .../docs/operations/agent-runtime-security.md | 22 +- website/docs/reference/api-reference.md | 62 +- website/docs/reference/cli-commands.md | 100 +- website/docs/reference/cli.md | 64 +- website/sidebars.js | 1 - website/src/components/QuickStartSection.js | 2 +- website/src/data/landingPageData.js | 4 +- workers/acp/images/README.md | 161 + workers/acp/images/artifact-tool.go | 289 + workers/acp/images/artifact-tool_test.go | 63 + workers/acp/images/claude/Dockerfile | 159 + workers/acp/images/codex/Dockerfile | 159 + workers/acp/images/codex/patch-agent-mode.mjs | 33 + workers/acp/images/copilot/Dockerfile | 98 + workers/acp/images/copilot/dockerfile_test.go | 112 + workers/acp/supervisor/artifact_broker.go | 97 + workers/acp/supervisor/artifact_client.go | 257 + .../supervisor/artifact_client_limits_test.go | 194 + .../artifact_client_materializer.go | 420 + .../artifact_client_nofollow_other.go | 15 + .../artifact_client_nofollow_unix.go | 23 + .../acp/supervisor/artifact_client_test.go | 465 + .../supervisor/artifact_client_uploader.go | 56 + workers/acp/supervisor/config.go | 167 + workers/acp/supervisor/env.go | 610 + .../env_artifact_client_limits_test.go | 128 + workers/acp/supervisor/env_test.go | 463 + workers/acp/supervisor/mapper.go | 159 + workers/acp/supervisor/mapper_test.go | 144 + workers/acp/supervisor/mcp_broker_client.go | 119 + .../acp/supervisor/mcp_broker_client_test.go | 171 + workers/acp/supervisor/mcp_proxy.go | 706 ++ workers/acp/supervisor/mcp_proxy_test.go | 295 + workers/acp/supervisor/prompt_handlers.go | 1690 +++ .../acp/supervisor/prompt_handlers_test.go | 825 ++ workers/acp/supervisor/provider_proxy.go | 937 ++ workers/acp/supervisor/provider_proxy_test.go | 833 ++ workers/acp/supervisor/server.go | 909 ++ workers/acp/supervisor/server_test.go | 1389 ++ .../supervisor/session_identity_lock_other.go | 13 + .../supervisor/session_identity_lock_unix.go | 45 + .../acp/supervisor/session_identity_state.go | 202 + .../supervisor/session_identity_state_test.go | 211 + .../supervisor/workspace_delta_limits_test.go | 53 + workers/common/git_identity_unix.go | 29 +- workers/general/main.go | 15 + workers/general/main_test.go | 50 + workers/harness/Dockerfile | 59 - workers/harness/cliwrapper/.gitignore | 4 - workers/harness/cliwrapper/adapter.go | 331 - workers/harness/cliwrapper/adapter_test.go | 501 - .../harness/cliwrapper/child_identity_unix.go | 265 - .../cliwrapper/child_identity_windows.go | 27 - workers/harness/cliwrapper/claude_adapter.go | 227 - .../harness/cliwrapper/claude_adapter_test.go | 236 - workers/harness/cliwrapper/codex_adapter.go | 406 - .../harness/cliwrapper/codex_adapter_test.go | 584 - workers/harness/cliwrapper/command_test.go | 262 - workers/harness/cliwrapper/config.go | 270 - .../harness/cliwrapper/conformance_test.go | 24 - workers/harness/cliwrapper/copilot_adapter.go | 216 - .../cliwrapper/copilot_adapter_test.go | 135 - workers/harness/cliwrapper/fake_adapter.go | 187 - workers/harness/cliwrapper/generic_adapter.go | 419 - workers/harness/cliwrapper/multi_adapter.go | 78 - .../harness/cliwrapper/multi_adapter_test.go | 23 - .../harness/cliwrapper/opencode_adapter.go | 511 - .../cliwrapper/opencode_adapter_test.go | 689 - workers/harness/cliwrapper/process_unix.go | 103 - workers/harness/cliwrapper/process_windows.go | 24 - workers/harness/cliwrapper/result.go | 404 - .../harness/cliwrapper/result_file_unix.go | 129 - .../harness/cliwrapper/result_file_windows.go | 101 - workers/harness/cliwrapper/runner.go | 278 - .../harness/cliwrapper/runtime_auth_proxy.go | 302 - .../cliwrapper/runtime_auth_proxy_test.go | 383 - workers/harness/cliwrapper/server.go | 1237 -- workers/harness/cliwrapper/server_test.go | 896 -- workers/harness/cliwrapper/turn_registry.go | 108 - workers/harness/cliwrapper/workspace.go | 438 - workers/harness/cliwrapper/workspace_test.go | 309 - workers/publisher/Dockerfile | 82 + workers/publisher/README.md | 50 + workers/publisher/dockerfile_test.go | 55 + 886 files changed, 156108 insertions(+), 62736 deletions(-) delete mode 100644 .github/workflows/agentruntime-external-e2e.yml create mode 100644 .github/workflows/live-acp-release-gate.yml create mode 100644 .github/workflows/live-acp-runtime-e2e.yml create mode 100644 api/v1alpha1/branch_claim_types.go create mode 100644 api/v1alpha1/control_record_types.go create mode 100644 api/v1alpha1/controller_epoch_types.go create mode 100644 api/v1alpha1/external_effect_types.go create mode 100644 api/v1alpha1/external_effect_types_test.go create mode 100644 api/v1alpha1/prompt_attempt_types.go create mode 100644 api/v1alpha1/publication_types.go create mode 100644 api/v1alpha1/runtime_pool_types.go create mode 100644 api/v1alpha1/runtime_pool_types_test.go create mode 100644 api/v1alpha1/runtime_session_control_types.go create mode 100644 api/v1alpha1/task_runtime_types.go create mode 100644 api/v1alpha1/task_runtime_types_test.go create mode 100644 api/v1alpha1/task_session_ref_validation_test.go create mode 100644 charts/orka/crds/branchclaim-customresourcedefinition.yaml create mode 100644 charts/orka/crds/controllerepoch-customresourcedefinition.yaml create mode 100644 charts/orka/crds/executionworkspace-customresourcedefinition.yaml create mode 100644 charts/orka/crds/executionworkspaceclass-customresourcedefinition.yaml create mode 100644 charts/orka/crds/executionworkspacepool-customresourcedefinition.yaml create mode 100644 charts/orka/crds/executionworkspaceprovider-customresourcedefinition.yaml create mode 100644 charts/orka/crds/externaleffect-customresourcedefinition.yaml create mode 100644 charts/orka/crds/fakepoolparameters-customresourcedefinition.yaml create mode 100644 charts/orka/crds/fakeproviderconfig-customresourcedefinition.yaml create mode 100644 charts/orka/crds/outboundaccesspolicy-customresourcedefinition.yaml create mode 100644 charts/orka/crds/promptattempt-customresourcedefinition.yaml create mode 100644 charts/orka/crds/publication-customresourcedefinition.yaml create mode 100644 charts/orka/crds/runtimepool-customresourcedefinition.yaml create mode 100644 charts/orka/crds/runtimesessioncontrol-customresourcedefinition.yaml create mode 100644 charts/orka/templates/acp-artifact-secret.yaml delete mode 100644 charts/orka/templates/harness-wrapper-deployment.yaml delete mode 100644 charts/orka/templates/harness-wrapper-secret.yaml create mode 100644 charts/orka/templates/provider-proxy-deployment.yaml create mode 100644 charts/orka/templates/provider-proxy-networkpolicy.yaml create mode 100644 charts/orka/templates/provider-proxy-secret.yaml create mode 100644 charts/orka/templates/provider-proxy-service.yaml create mode 100644 charts/orka/templates/provider-proxy-serviceaccount.yaml create mode 100644 charts/orka/templates/publisher-deployment.yaml create mode 100644 charts/orka/templates/publisher-networkpolicy.yaml create mode 100644 charts/orka/templates/publisher-pvc.yaml create mode 100644 charts/orka/templates/publisher-secret.yaml rename cmd/build/helmify/static/templates/harness-wrapper-service.yaml => charts/orka/templates/publisher-service.yaml (54%) create mode 100644 charts/orka/templates/runtime-namespace.yaml create mode 100644 charts/orka/templates/scm-egress-proxy-deployment.yaml create mode 100644 charts/orka/templates/scm-egress-proxy-networkpolicy.yaml create mode 100644 charts/orka/templates/scm-egress-proxy-secret.yaml create mode 100644 charts/orka/templates/scm-egress-proxy-service.yaml create mode 100644 charts/orka/templates/scm-egress-proxy-serviceaccount.yaml create mode 100644 charts/orka/templates/vekil-ingress-networkpolicy.yaml create mode 100644 cmd/build/helmify/static/templates/acp-artifact-secret.yaml delete mode 100644 cmd/build/helmify/static/templates/harness-wrapper-deployment.yaml delete mode 100644 cmd/build/helmify/static/templates/harness-wrapper-secret.yaml create mode 100644 cmd/build/helmify/static/templates/provider-proxy-deployment.yaml create mode 100644 cmd/build/helmify/static/templates/provider-proxy-networkpolicy.yaml create mode 100644 cmd/build/helmify/static/templates/provider-proxy-secret.yaml create mode 100644 cmd/build/helmify/static/templates/provider-proxy-service.yaml create mode 100644 cmd/build/helmify/static/templates/provider-proxy-serviceaccount.yaml create mode 100644 cmd/build/helmify/static/templates/publisher-deployment.yaml create mode 100644 cmd/build/helmify/static/templates/publisher-networkpolicy.yaml create mode 100644 cmd/build/helmify/static/templates/publisher-pvc.yaml create mode 100644 cmd/build/helmify/static/templates/publisher-secret.yaml rename manifest_staging/charts/orka/templates/harness-wrapper-service.yaml => cmd/build/helmify/static/templates/publisher-service.yaml (54%) create mode 100644 cmd/build/helmify/static/templates/runtime-namespace.yaml create mode 100644 cmd/build/helmify/static/templates/scm-egress-proxy-deployment.yaml create mode 100644 cmd/build/helmify/static/templates/scm-egress-proxy-networkpolicy.yaml create mode 100644 cmd/build/helmify/static/templates/scm-egress-proxy-secret.yaml create mode 100644 cmd/build/helmify/static/templates/scm-egress-proxy-service.yaml create mode 100644 cmd/build/helmify/static/templates/scm-egress-proxy-serviceaccount.yaml create mode 100644 cmd/build/helmify/static/templates/vekil-ingress-networkpolicy.yaml delete mode 100644 cmd/build/helmify/static/templates/workspace-class-use-policy.yaml delete mode 100644 cmd/build/helmify/static/templates/workspace-class-use-webhook.yaml delete mode 100644 cmd/build/helmify/static/templates/workspace-core-admission-policy.yaml delete mode 100644 cmd/build/helmify/static/templates/workspace-crd-upgrade-guard.yaml create mode 100644 cmd/cli/runtime.go create mode 100644 cmd/cli/runtime_test.go create mode 100644 cmd/cli/task_workspace.go create mode 100644 cmd/cli/task_workspace_test.go create mode 100644 cmd/orka-acp-exec-helper/main.go create mode 100644 cmd/orka-acp-runtime/main.go delete mode 100644 cmd/orka-agent-harness-wrapper/.gitignore delete mode 100644 cmd/orka-agent-harness-wrapper/main.go create mode 100644 cmd/orka-image-ref-validator/main.go create mode 100644 cmd/orka-image-ref-validator/main_test.go create mode 100644 cmd/orka-provider-auth-proxy/credentials.go create mode 100644 cmd/orka-provider-auth-proxy/credentials_test.go create mode 100644 cmd/orka-provider-auth-proxy/main.go create mode 100644 cmd/orka-provider-auth-proxy/proxy.go create mode 100644 cmd/orka-provider-auth-proxy/proxy_test.go create mode 100644 cmd/orka-scm-egress-proxy/auth.go create mode 100644 cmd/orka-scm-egress-proxy/config.go create mode 100644 cmd/orka-scm-egress-proxy/main.go create mode 100644 cmd/orka-scm-egress-proxy/network.go create mode 100644 cmd/orka-scm-egress-proxy/proxy.go create mode 100644 cmd/orka-scm-egress-proxy/proxy_test.go create mode 100644 cmd/orka-workspace-publisher/main.go create mode 100644 cmd/orka-workspace-publisher/umask_other.go create mode 100644 cmd/orka-workspace-publisher/umask_unix.go create mode 100644 config/acp-production/README.md create mode 100644 config/acp-production/kustomization.yaml create mode 100644 config/acp-production/runtime-images.env create mode 100644 config/acp-workload/README.md rename config/{harness-wrapper/service.yaml => acp-workload/api_service.yaml} (57%) create mode 100644 config/acp-workload/kustomization.yaml create mode 100644 config/acp-workload/kustomizeconfig.yaml create mode 100644 config/acp-workload/manager_metrics_patch.yaml create mode 100644 config/acp-workload/metrics_service.yaml create mode 100644 config/acp-workload/remove_admission_policy_binding_namespace.yaml create mode 100644 config/acp-workload/remove_admission_policy_namespace.yaml create mode 100644 config/crd/bases/core.orka.ai_branchclaims.yaml create mode 100644 config/crd/bases/core.orka.ai_controllerepochs.yaml create mode 100644 config/crd/bases/core.orka.ai_externaleffects.yaml create mode 100644 config/crd/bases/core.orka.ai_promptattempts.yaml create mode 100644 config/crd/bases/core.orka.ai_publications.yaml create mode 100644 config/crd/bases/core.orka.ai_runtimepools.yaml create mode 100644 config/crd/bases/core.orka.ai_runtimesessioncontrols.yaml delete mode 100644 config/harness-wrapper/README.md delete mode 100644 config/harness-wrapper/deployment.yaml delete mode 100644 config/harness-wrapper/kustomization.yaml delete mode 100644 config/harness-wrapper/serviceaccount.yaml delete mode 100644 config/harness-wrapper/volume-mode-patch.yaml create mode 100644 config/provider-proxy/deployment.yaml create mode 100644 config/provider-proxy/kustomization.yaml create mode 100644 config/provider-proxy/networkpolicy.yaml create mode 100644 config/provider-proxy/service.yaml create mode 100644 config/provider-proxy/serviceaccount.yaml create mode 100644 config/publisher/deployment.yaml create mode 100644 config/publisher/kustomization.yaml create mode 100644 config/publisher/networkpolicy.yaml create mode 100644 config/publisher/pvc.yaml create mode 100644 config/publisher/service.yaml create mode 100644 config/publisher/serviceaccount.yaml delete mode 100644 config/samples/core_v1alpha1_agent_opencode.yaml delete mode 100644 config/samples/core_v1alpha1_agentruntime_agentkit.yaml delete mode 100644 config/samples/core_v1alpha1_agentruntime_foundry.yaml delete mode 100644 config/samples/core_v1alpha1_task_agent_copilot.yaml create mode 100644 config/scm-egress-proxy/README.md create mode 100644 config/scm-egress-proxy/deployment.yaml create mode 100644 config/scm-egress-proxy/kustomization.yaml create mode 100644 config/scm-egress-proxy/networkpolicy.yaml create mode 100644 config/scm-egress-proxy/service.yaml create mode 100644 config/scm-egress-proxy/serviceaccount.yaml create mode 100644 config/vekil-ingress/kustomization.yaml create mode 100644 config/vekil-ingress/networkpolicy.yaml delete mode 100644 docs/adr/0009-agentkit-byoa-harness-runtime-boundary.md create mode 100644 docs/development/acp-crd-hard-cutover.md delete mode 100644 docs/development/harness-conformance.md delete mode 100644 docs/development/harness-protocol-mvp.md delete mode 100644 examples/bring-your-own-agent-runtime-demo/README.md delete mode 100644 examples/bring-your-own-agent-runtime-demo/SECURITY.md delete mode 100644 examples/fibey-custom-agent-demo/README.md delete mode 100644 examples/fibey-custom-agent-demo/agent-agentkit.yaml delete mode 100644 examples/fibey-custom-agent-demo/agent-foundry.yaml delete mode 100644 examples/fibey-custom-agent-demo/agent.yaml delete mode 100644 examples/fibey-custom-agent-demo/agentkit-runtime-offline.example.yaml delete mode 100644 examples/fibey-custom-agent-demo/agentruntime-agentkit.yaml delete mode 100644 examples/fibey-custom-agent-demo/agentruntime-foundry.yaml delete mode 100644 examples/fibey-custom-agent-demo/agentruntime.yaml delete mode 100644 examples/fibey-custom-agent-demo/kustomization.yaml delete mode 100644 examples/fibey-custom-agent-demo/mock-http-runtime-service.yaml delete mode 100644 examples/fibey-custom-agent-demo/secret-agentkit.yaml delete mode 100644 examples/fibey-custom-agent-demo/secret-foundry.yaml delete mode 100644 examples/fibey-custom-agent-demo/secret.yaml delete mode 100755 examples/fibey-custom-agent-demo/switch-backend.sh delete mode 100644 examples/fibey-custom-agent-demo/task.yaml delete mode 100644 examples/harness/echo/Dockerfile delete mode 100644 examples/harness/echo/main.go delete mode 100644 examples/harness/echo/main_test.go delete mode 100644 examples/support-escalation-runtime-demo/README.md delete mode 100644 examples/support-escalation-runtime-demo/agent.yaml delete mode 100644 examples/support-escalation-runtime-demo/agentruntime.yaml delete mode 100644 examples/support-escalation-runtime-demo/kustomization.yaml delete mode 100644 examples/support-escalation-runtime-demo/mock-http-runtime-service.yaml delete mode 100644 examples/support-escalation-runtime-demo/mock-support-tool-service.yaml delete mode 100644 examples/support-escalation-runtime-demo/task.yaml delete mode 100644 examples/support-escalation-runtime-demo/tools.yaml create mode 100644 hack/agent-substrate/README.md create mode 100644 hack/agent-substrate/atenet-router-authorization-redaction.patch create mode 100644 hack/agent-substrate/ateom-runsc-delete-recovery.patch create mode 100644 internal/acp/client.go create mode 100644 internal/acp/client_test.go create mode 100644 internal/acp/pins.go create mode 100644 internal/acp/process.go create mode 100644 internal/acp/process_linux.go create mode 100644 internal/acp/process_linux_test.go create mode 100644 internal/acp/process_test.go create mode 100644 internal/acp/process_test_support_other_test.go create mode 100644 internal/acp/process_unix.go create mode 100644 internal/acp/process_windows.go create mode 100644 internal/acp/protocol.go create mode 100644 internal/acp/runtime_env.go create mode 100644 internal/acp/runtime_env_test.go create mode 100644 internal/acp/session.go create mode 100644 internal/acp/session_test.go create mode 100644 internal/acp/supervisor_hardening.go create mode 100644 internal/acp/supervisor_hardening_linux.go create mode 100644 internal/acp/supervisor_hardening_nonunix.go create mode 100644 internal/acp/supervisor_hardening_unix.go create mode 100644 internal/api/acp_artifact_authorization.go create mode 100644 internal/api/acp_artifact_authorization_test.go create mode 100644 internal/api/acp_artifact_handlers.go create mode 100644 internal/api/acp_artifact_handlers_test.go create mode 100644 internal/api/acp_credential_broker.go create mode 100644 internal/api/acp_credential_broker_continuation_test.go create mode 100644 internal/api/acp_credential_broker_test.go create mode 100644 internal/api/acp_mcp_broker_registration.go create mode 100644 internal/api/acp_mcp_broker_registration_test.go create mode 100644 internal/api/runtime_resource_handlers.go create mode 100644 internal/api/runtime_resource_handlers_test.go create mode 100644 internal/api/task_workspace.go create mode 100644 internal/api/task_workspace_test.go create mode 100644 internal/artifactcap/capability.go create mode 100644 internal/artifactcap/capability_test.go create mode 100644 internal/artifactcap/digest.go create mode 100644 internal/artifactcap/io_helpers.go create mode 100644 internal/artifactcap/ledger.go create mode 100644 internal/artifactcap/nofollow_other.go create mode 100644 internal/artifactcap/nofollow_unix.go create mode 100644 internal/artifactcap/retention.go create mode 100644 internal/artifactcap/retention_test.go create mode 100644 internal/artifactcap/root_lock_gate.go create mode 100644 internal/artifactcap/root_lock_other.go create mode 100644 internal/artifactcap/root_lock_unix.go create mode 100644 internal/artifactcap/service.go create mode 100644 internal/artifactcap/service_test.go create mode 100644 internal/artifactcap/store.go create mode 100644 internal/artifactcap/types.go create mode 100644 internal/controller/acp_agent_configuration.go create mode 100644 internal/controller/acp_agent_configuration_skills_test.go create mode 100644 internal/controller/acp_agent_configuration_test.go create mode 100644 internal/controller/acp_agent_configuration_tools_test.go create mode 100644 internal/controller/acp_artifact_retention_test.go create mode 100644 internal/controller/acp_credentials.go create mode 100644 internal/controller/acp_dispatcher.go create mode 100644 internal/controller/acp_dispatcher_prompt_attempt_settlement_test.go create mode 100644 internal/controller/acp_dispatcher_session.go create mode 100644 internal/controller/acp_dispatcher_session_bootstrap_test.go create mode 100644 internal/controller/acp_dispatcher_test.go create mode 100644 internal/controller/acp_external_effect.go create mode 100644 internal/controller/acp_frozen_authority.go create mode 100644 internal/controller/acp_frozen_authority_settlement_test.go create mode 100644 internal/controller/acp_idle_pool_test.go create mode 100644 internal/controller/acp_mcp_broker.go create mode 100644 internal/controller/acp_mcp_broker_coordination_registration_test.go create mode 100644 internal/controller/acp_mcp_broker_test.go create mode 100644 internal/controller/acp_mcp_policy.go create mode 100644 internal/controller/acp_mcp_policy_test.go create mode 100644 internal/controller/acp_outbox.go create mode 100644 internal/controller/acp_outbox_test.go create mode 100644 internal/controller/acp_publication.go create mode 100644 internal/controller/acp_publication_receipt_test.go create mode 100644 internal/controller/acp_publication_reclamation_test.go create mode 100644 internal/controller/acp_publication_recovery.go create mode 100644 internal/controller/acp_recovery.go create mode 100644 internal/controller/acp_recovery_test.go create mode 100644 internal/controller/acp_runtime_pool_reserved_rebind_test.go create mode 100644 internal/controller/acp_runtime_pool_rotation_rebind_test.go create mode 100644 internal/controller/acp_runtime_profile.go create mode 100644 internal/controller/acp_runtime_profile_test.go create mode 100644 internal/controller/acp_session.go create mode 100644 internal/controller/acp_session_json.go create mode 100644 internal/controller/acp_session_recovery.go create mode 100644 internal/controller/acp_session_recovery_test.go create mode 100644 internal/controller/acp_session_runtime.go create mode 100644 internal/controller/acp_session_test.go create mode 100644 internal/controller/acp_session_transcript.go create mode 100644 internal/controller/acp_task_finalizer.go create mode 100644 internal/controller/acp_task_finalizer_test.go create mode 100644 internal/controller/acp_task_queue.go create mode 100644 internal/controller/acp_task_queue_reconciliation_test.go create mode 100644 internal/controller/acp_task_queue_test.go create mode 100644 internal/controller/acp_upgrade_drain.go create mode 100644 internal/controller/acp_upgrade_drain_test.go create mode 100644 internal/controller/acp_workspace.go create mode 100644 internal/controller/acp_workspace_repository_identity_casing_test.go create mode 100644 internal/controller/acp_workspace_test.go create mode 100644 internal/controller/agent_execution_plan_unsupported_semantics_test.go create mode 100644 internal/controller/controller_epoch.go create mode 100644 internal/controller/controller_epoch_test.go delete mode 100644 internal/controller/harness_broker.go delete mode 100644 internal/controller/harness_wrapper.go delete mode 100644 internal/controller/harness_wrapper_test.go create mode 100644 internal/controller/runtime_pool_controller.go create mode 100644 internal/controller/runtime_pool_controller_test.go create mode 100644 internal/controller/session_cleanup_recovery.go create mode 100644 internal/controller/session_cleanup_recovery_test.go create mode 100644 internal/controller/task_workspace_credentials.go delete mode 100644 internal/harness/client.go delete mode 100644 internal/harness/client_test.go delete mode 100644 internal/harness/conformance/agentkit_external_test.go delete mode 100644 internal/harness/conformance/conformance.go delete mode 100644 internal/harness/conformance/conformance_test.go delete mode 100644 internal/harness/frame_identity.go delete mode 100644 internal/harness/frame_identity_test.go delete mode 100644 internal/harness/harnesstest/conformance.go delete mode 100644 internal/harness/harnesstest/fake_server.go delete mode 100644 internal/harness/harnesstest/fake_server_test.go delete mode 100644 internal/harness/lifecycle.go delete mode 100644 internal/harness/lifecycle_test.go delete mode 100644 internal/harness/mapper.go delete mode 100644 internal/harness/mapper_test.go delete mode 100644 internal/harness/protocol.go delete mode 100644 internal/harness/protocol_test.go delete mode 100644 internal/harness/runner.go delete mode 100644 internal/harness/runner_test.go delete mode 100644 internal/harness/runtime_session_identity.go delete mode 100644 internal/harness/runtime_session_identity_test.go delete mode 100644 internal/harness/runtime_session_store.go delete mode 100644 internal/harness/transport.go delete mode 100644 internal/harness/turn_journal.go delete mode 100644 internal/harness/turn_journal_test.go delete mode 100644 internal/harness/turn_path.go delete mode 100644 internal/harness/turn_path_test.go create mode 100644 internal/harness/v2/capability.go create mode 100644 internal/harness/v2/capability_test.go create mode 100644 internal/harness/v2/classification.go create mode 100644 internal/harness/v2/classification_lifecycle_test.go create mode 100644 internal/harness/v2/client.go create mode 100644 internal/harness/v2/client_errors.go create mode 100644 internal/harness/v2/client_stream.go create mode 100644 internal/harness/v2/client_stream_test.go create mode 100644 internal/harness/v2/client_test.go create mode 100644 internal/harness/v2/conformance/check.go create mode 100644 internal/harness/v2/conformance/check_test.go create mode 100644 internal/harness/v2/conformance/conformancetest/server.go create mode 100644 internal/harness/v2/conformance/cycle.go create mode 100644 internal/harness/v2/conformance/doc.go create mode 100644 internal/harness/v2/conformance/replay.go create mode 100644 internal/harness/v2/conformance/types.go create mode 100644 internal/harness/v2/digest.go create mode 100644 internal/harness/v2/doc.go create mode 100644 internal/harness/v2/event_types.go create mode 100644 internal/harness/v2/identity.go create mode 100644 internal/harness/v2/lifecycle.go create mode 100644 internal/harness/v2/mcp.go create mode 100644 internal/harness/v2/mcp_test.go create mode 100644 internal/harness/v2/ndjson.go create mode 100644 internal/harness/v2/ndjson_test.go create mode 100644 internal/harness/v2/paths.go create mode 100644 internal/harness/v2/paths_digest_test.go create mode 100644 internal/harness/v2/prompt.go create mode 100644 internal/harness/v2/protocol.go create mode 100644 internal/harness/v2/semantics.go create mode 100644 internal/harness/v2/session.go create mode 100644 internal/harness/v2/session_prompt_workspace_test.go create mode 100644 internal/harness/v2/test_helpers_test.go create mode 100644 internal/harness/v2/validation.go create mode 100644 internal/harness/v2/workspace.go create mode 100644 internal/publisher/artifact.go create mode 100644 internal/publisher/command_other.go create mode 100644 internal/publisher/command_unix.go create mode 100644 internal/publisher/doc.go create mode 100644 internal/publisher/errors.go create mode 100644 internal/publisher/git.go create mode 100644 internal/publisher/git_test.go create mode 100644 internal/publisher/pr.go create mode 100644 internal/publisher/prepare.go create mode 100644 internal/publisher/proxy_environment.go create mode 100644 internal/publisher/proxy_environment_test.go create mode 100644 internal/publisher/publish.go create mode 100644 internal/publisher/publisher.go create mode 100644 internal/publisher/publisher_test.go create mode 100644 internal/publisher/reclaim.go create mode 100644 internal/publisher/service/artifact_authorization.go create mode 100644 internal/publisher/service/artifact_authorization_test.go create mode 100644 internal/publisher/service/artifact_client.go create mode 100644 internal/publisher/service/capability.go create mode 100644 internal/publisher/service/capability_test.go create mode 100644 internal/publisher/service/client.go create mode 100644 internal/publisher/service/config.go create mode 100644 internal/publisher/service/credential.go create mode 100644 internal/publisher/service/credential_broker.go create mode 100644 internal/publisher/service/credential_broker_test.go create mode 100644 internal/publisher/service/development_fallback_test.go create mode 100644 internal/publisher/service/doc.go create mode 100644 internal/publisher/service/env.go create mode 100644 internal/publisher/service/env_test.go create mode 100644 internal/publisher/service/errors.go create mode 100644 internal/publisher/service/git.go create mode 100644 internal/publisher/service/git_workspace_test.go create mode 100644 internal/publisher/service/github_pr.go create mode 100644 internal/publisher/service/github_pr_test.go create mode 100644 internal/publisher/service/handlers.go create mode 100644 internal/publisher/service/journal.go create mode 100644 internal/publisher/service/journal_test.go create mode 100644 internal/publisher/service/process_other.go create mode 100644 internal/publisher/service/process_unix.go create mode 100644 internal/publisher/service/proxy_environment_test.go create mode 100644 internal/publisher/service/server.go create mode 100644 internal/publisher/service/service_test.go create mode 100644 internal/publisher/service/types.go create mode 100644 internal/publisher/service/validation.go create mode 100644 internal/publisher/service/workspace.go create mode 100644 internal/publisher/storage.go create mode 100644 internal/publisher/types.go create mode 100644 internal/publisher/validation.go create mode 100644 internal/publisher/validation_test.go create mode 100644 internal/publisher/verify.go create mode 100644 internal/safesymlink/symlink.go create mode 100644 internal/safesymlink/symlink_test.go create mode 100644 internal/store/control_store.go create mode 100644 internal/store/control_types.go create mode 100644 internal/store/control_types_test.go create mode 100644 internal/store/kube/branch_claim.go create mode 100644 internal/store/kube/branch_claim_reclamation_test.go create mode 100644 internal/store/kube/controller_epoch.go create mode 100644 internal/store/kube/external_effect.go create mode 100644 internal/store/kube/helpers.go create mode 100644 internal/store/kube/outbox.go create mode 100644 internal/store/kube/prompt_attempt.go create mode 100644 internal/store/kube/prompt_attempt_reclaim_test.go create mode 100644 internal/store/kube/publication.go create mode 100644 internal/store/kube/session_cleanup.go create mode 100644 internal/store/kube/session_cleanup_test.go create mode 100644 internal/store/kube/session_control.go create mode 100644 internal/store/kube/session_turn.go create mode 100644 internal/store/kube/store.go create mode 100644 internal/store/kube/store_test.go create mode 100644 internal/store/outbox_persistence.go create mode 100644 internal/store/session_cleanup.go create mode 100644 internal/store/session_turn_persistence.go create mode 100644 internal/store/sqlite/branch_claim_reclamation_test.go create mode 100644 internal/store/sqlite/branch_claim_store.go create mode 100644 internal/store/sqlite/control_helpers.go create mode 100644 internal/store/sqlite/control_migrations.go create mode 100644 internal/store/sqlite/control_store_test.go create mode 100644 internal/store/sqlite/controller_epoch_store.go create mode 100644 internal/store/sqlite/external_effect_store.go create mode 100644 internal/store/sqlite/outbox_store.go create mode 100644 internal/store/sqlite/prompt_attempt_store.go create mode 100644 internal/store/sqlite/publication_store.go delete mode 100644 internal/store/sqlite/runtime_session_store.go delete mode 100644 internal/store/sqlite/runtime_session_store_test.go create mode 100644 internal/store/sqlite/session_cleanup.go create mode 100644 internal/store/sqlite/session_cleanup_test.go create mode 100644 internal/store/sqlite/session_control_store.go create mode 100644 internal/store/sqlite/session_delete_control_test.go create mode 100644 internal/store/sqlite/session_turn_persistence.go create mode 100644 internal/store/sqlite/session_turn_persistence_test.go create mode 100644 internal/tools/broker_coordination_registration_test.go create mode 100644 internal/workspacedelta/archive.go create mode 100644 internal/workspacedelta/build_limits_test.go create mode 100644 internal/workspacedelta/compare.go create mode 100644 internal/workspacedelta/doc.go create mode 100644 internal/workspacedelta/errors.go create mode 100644 internal/workspacedelta/nofollow_unix.go create mode 100644 internal/workspacedelta/nofollow_unsupported.go create mode 100644 internal/workspacedelta/path.go create mode 100644 internal/workspacedelta/policy.go create mode 100644 internal/workspacedelta/security_unix_test.go create mode 100644 internal/workspacedelta/snapshot.go create mode 100644 internal/workspacedelta/types.go create mode 100644 internal/workspacedelta/workspacedelta_test.go create mode 100644 manifest_staging/charts/orka/crds/branchclaim-customresourcedefinition.yaml create mode 100644 manifest_staging/charts/orka/crds/controllerepoch-customresourcedefinition.yaml create mode 100644 manifest_staging/charts/orka/crds/externaleffect-customresourcedefinition.yaml create mode 100644 manifest_staging/charts/orka/crds/promptattempt-customresourcedefinition.yaml create mode 100644 manifest_staging/charts/orka/crds/publication-customresourcedefinition.yaml create mode 100644 manifest_staging/charts/orka/crds/runtimepool-customresourcedefinition.yaml create mode 100644 manifest_staging/charts/orka/crds/runtimesessioncontrol-customresourcedefinition.yaml create mode 100644 manifest_staging/charts/orka/templates/acp-artifact-secret.yaml delete mode 100644 manifest_staging/charts/orka/templates/harness-wrapper-deployment.yaml delete mode 100644 manifest_staging/charts/orka/templates/harness-wrapper-secret.yaml create mode 100644 manifest_staging/charts/orka/templates/provider-proxy-deployment.yaml create mode 100644 manifest_staging/charts/orka/templates/provider-proxy-networkpolicy.yaml create mode 100644 manifest_staging/charts/orka/templates/provider-proxy-secret.yaml create mode 100644 manifest_staging/charts/orka/templates/provider-proxy-service.yaml create mode 100644 manifest_staging/charts/orka/templates/provider-proxy-serviceaccount.yaml create mode 100644 manifest_staging/charts/orka/templates/publisher-deployment.yaml create mode 100644 manifest_staging/charts/orka/templates/publisher-networkpolicy.yaml create mode 100644 manifest_staging/charts/orka/templates/publisher-pvc.yaml create mode 100644 manifest_staging/charts/orka/templates/publisher-secret.yaml rename charts/orka/templates/harness-wrapper-service.yaml => manifest_staging/charts/orka/templates/publisher-service.yaml (54%) create mode 100644 manifest_staging/charts/orka/templates/runtime-namespace.yaml create mode 100644 manifest_staging/charts/orka/templates/scm-egress-proxy-deployment.yaml create mode 100644 manifest_staging/charts/orka/templates/scm-egress-proxy-networkpolicy.yaml create mode 100644 manifest_staging/charts/orka/templates/scm-egress-proxy-secret.yaml create mode 100644 manifest_staging/charts/orka/templates/scm-egress-proxy-service.yaml create mode 100644 manifest_staging/charts/orka/templates/scm-egress-proxy-serviceaccount.yaml create mode 100644 manifest_staging/charts/orka/templates/vekil-ingress-networkpolicy.yaml delete mode 100644 manifest_staging/charts/orka/templates/workspace-class-use-policy.yaml delete mode 100644 manifest_staging/charts/orka/templates/workspace-class-use-webhook.yaml delete mode 100644 manifest_staging/charts/orka/templates/workspace-core-admission-policy.yaml delete mode 100644 manifest_staging/charts/orka/templates/workspace-crd-upgrade-guard.yaml delete mode 100644 pkg/harness/conformance/conformance.go delete mode 100644 pkg/harness/conformance/doc.go delete mode 100644 pkg/harness/doc.go delete mode 100644 pkg/harness/harness.go delete mode 100644 pkg/harness/harness_test.go create mode 100755 scripts/apply-acp-production.sh create mode 100755 scripts/check-legacy-wrapper-resources.sh create mode 100755 scripts/lib/kind-local-registry.sh create mode 100644 scripts/lib/live-acp-runtime-kind-bootstrap.sh create mode 100755 scripts/live-acp-runtime-e2e.sh create mode 100755 scripts/live-acp-runtime-kind-e2e.sh create mode 100755 scripts/render-acp-runtime-images.sh create mode 100755 scripts/render-worker-images.sh create mode 100755 scripts/sync-helm-crds.sh create mode 100755 scripts/tests/acp-runtime-image-rollout-test.sh create mode 100755 scripts/tests/acp-secret-mounts-test.sh create mode 100755 scripts/tests/agent-substrate-e2e-hardening-test.sh create mode 100755 scripts/tests/agent-substrate-patches-test.sh create mode 100755 scripts/tests/live-acp-runtime-e2e-test.sh create mode 100755 scripts/tests/live-acp-runtime-kind-e2e-test.sh create mode 100755 scripts/tests/live-acp-runtime-port-forward-cleanup-test.sh create mode 100755 scripts/tests/sync-helm-crds-test.sh create mode 100755 scripts/tests/upgrade-orka-crds-test.sh create mode 100755 scripts/upgrade-orka-crds.sh create mode 100644 test/e2e/live_agent_runtime_matrix_manifest_unit_test.go create mode 100644 ui/src/components/runtime/runtime-registry.test.tsx create mode 100644 ui/src/components/runtime/runtime-registry.tsx create mode 100644 ui/src/hooks/use-runtimes.test.tsx create mode 100644 ui/src/hooks/use-runtimes.ts create mode 100644 ui/src/routes/runtimes/index.tsx create mode 100644 ui/src/schemas/runtime.test.ts create mode 100644 ui/src/schemas/runtime.ts delete mode 100644 website/docs/guides/cli-harness-wrapper.md create mode 100644 workers/acp/images/README.md create mode 100644 workers/acp/images/artifact-tool.go create mode 100644 workers/acp/images/artifact-tool_test.go create mode 100644 workers/acp/images/claude/Dockerfile create mode 100644 workers/acp/images/codex/Dockerfile create mode 100644 workers/acp/images/codex/patch-agent-mode.mjs create mode 100644 workers/acp/images/copilot/Dockerfile create mode 100644 workers/acp/images/copilot/dockerfile_test.go create mode 100644 workers/acp/supervisor/artifact_broker.go create mode 100644 workers/acp/supervisor/artifact_client.go create mode 100644 workers/acp/supervisor/artifact_client_limits_test.go create mode 100644 workers/acp/supervisor/artifact_client_materializer.go create mode 100644 workers/acp/supervisor/artifact_client_nofollow_other.go create mode 100644 workers/acp/supervisor/artifact_client_nofollow_unix.go create mode 100644 workers/acp/supervisor/artifact_client_test.go create mode 100644 workers/acp/supervisor/artifact_client_uploader.go create mode 100644 workers/acp/supervisor/config.go create mode 100644 workers/acp/supervisor/env.go create mode 100644 workers/acp/supervisor/env_artifact_client_limits_test.go create mode 100644 workers/acp/supervisor/env_test.go create mode 100644 workers/acp/supervisor/mapper.go create mode 100644 workers/acp/supervisor/mapper_test.go create mode 100644 workers/acp/supervisor/mcp_broker_client.go create mode 100644 workers/acp/supervisor/mcp_broker_client_test.go create mode 100644 workers/acp/supervisor/mcp_proxy.go create mode 100644 workers/acp/supervisor/mcp_proxy_test.go create mode 100644 workers/acp/supervisor/prompt_handlers.go create mode 100644 workers/acp/supervisor/prompt_handlers_test.go create mode 100644 workers/acp/supervisor/provider_proxy.go create mode 100644 workers/acp/supervisor/provider_proxy_test.go create mode 100644 workers/acp/supervisor/server.go create mode 100644 workers/acp/supervisor/server_test.go create mode 100644 workers/acp/supervisor/session_identity_lock_other.go create mode 100644 workers/acp/supervisor/session_identity_lock_unix.go create mode 100644 workers/acp/supervisor/session_identity_state.go create mode 100644 workers/acp/supervisor/session_identity_state_test.go create mode 100644 workers/acp/supervisor/workspace_delta_limits_test.go delete mode 100644 workers/harness/Dockerfile delete mode 100644 workers/harness/cliwrapper/.gitignore delete mode 100644 workers/harness/cliwrapper/adapter.go delete mode 100644 workers/harness/cliwrapper/adapter_test.go delete mode 100644 workers/harness/cliwrapper/child_identity_unix.go delete mode 100644 workers/harness/cliwrapper/child_identity_windows.go delete mode 100644 workers/harness/cliwrapper/claude_adapter.go delete mode 100644 workers/harness/cliwrapper/claude_adapter_test.go delete mode 100644 workers/harness/cliwrapper/codex_adapter.go delete mode 100644 workers/harness/cliwrapper/codex_adapter_test.go delete mode 100644 workers/harness/cliwrapper/command_test.go delete mode 100644 workers/harness/cliwrapper/config.go delete mode 100644 workers/harness/cliwrapper/conformance_test.go delete mode 100644 workers/harness/cliwrapper/copilot_adapter.go delete mode 100644 workers/harness/cliwrapper/copilot_adapter_test.go delete mode 100644 workers/harness/cliwrapper/fake_adapter.go delete mode 100644 workers/harness/cliwrapper/generic_adapter.go delete mode 100644 workers/harness/cliwrapper/multi_adapter.go delete mode 100644 workers/harness/cliwrapper/multi_adapter_test.go delete mode 100644 workers/harness/cliwrapper/opencode_adapter.go delete mode 100644 workers/harness/cliwrapper/opencode_adapter_test.go delete mode 100644 workers/harness/cliwrapper/process_unix.go delete mode 100644 workers/harness/cliwrapper/process_windows.go delete mode 100644 workers/harness/cliwrapper/result.go delete mode 100644 workers/harness/cliwrapper/result_file_unix.go delete mode 100644 workers/harness/cliwrapper/result_file_windows.go delete mode 100644 workers/harness/cliwrapper/runner.go delete mode 100644 workers/harness/cliwrapper/runtime_auth_proxy.go delete mode 100644 workers/harness/cliwrapper/runtime_auth_proxy_test.go delete mode 100644 workers/harness/cliwrapper/server.go delete mode 100644 workers/harness/cliwrapper/server_test.go delete mode 100644 workers/harness/cliwrapper/turn_registry.go delete mode 100644 workers/harness/cliwrapper/workspace.go delete mode 100644 workers/harness/cliwrapper/workspace_test.go create mode 100644 workers/publisher/Dockerfile create mode 100644 workers/publisher/README.md create mode 100644 workers/publisher/dockerfile_test.go diff --git a/.agents/skills/agent-sandbox-deploy/SKILL.md b/.agents/skills/agent-sandbox-deploy/SKILL.md index f1533d6a4..1ea774acd 100644 --- a/.agents/skills/agent-sandbox-deploy/SKILL.md +++ b/.agents/skills/agent-sandbox-deploy/SKILL.md @@ -108,9 +108,8 @@ test -x "$kindctl" "$orka_kind_deploy" ``` - The harness-wrapper image must be present for the separate plain-agent model - smoke. The model-free direct workspace-adapter smoke bypasses the Task/harness - path. + The digest-pinned ACP runtime images must be present for the separate plain-agent + model smoke. The model-free direct workspace-adapter smoke bypasses the Task-to-RuntimeSession path. 3. **Install agent-sandbox** by driving the canonical script against the kindctl kubeconfig. Export `KUBECONFIG` from kindctl so the script's `kubectl` calls @@ -208,12 +207,132 @@ test -x "$kindctl" wget -qO- http://127.0.0.1:1337/readyz ``` - If you only need model-free confidence, run the CI parity script from the - `Model-free CI parity` section of `references/validate.md`. It validates - installation/configuration plus the direct workspace-adapter lifecycle - (SandboxClaim readiness, router exec, delete, retained reuse, and claim - cleanup); - only the full workspace-backed agent Task path remains gated. + If you only need model-free confidence, run the CI parity script below. It + validates installation/configuration only while workspace-backed agent Tasks + remain gated; it is not a claim/readiness/exec/cleanup smoke. + +## Validate + +> **Current boundary:** this skill validates the upstream agent-sandbox +> provider directly. Orka ACP RuntimeSessions do not yet map to sandbox claims; +> execution-workspace-backed agent Tasks remain expected-future evidence. The +> removed v1 harness-wrapper path must not be reintroduced. Validate plain +> Codex/Claude ACP Tasks with `scripts/live-acp-runtime-e2e.sh`. + +Do **not** use an execution-workspace agent Task as the success criterion yet. +Validate the two currently wired paths separately: + +- **Model path through ACP** (requires the optional `AGENTIC=1` step and + vekil ready): run a plain agent Task with no `execution.workspace` and wait + for it to succeed. + +```bash +"$kindctl" kubectl -n demo-magic apply -f - <<'YAML' +apiVersion: core.orka.ai/v1alpha1 +kind: Agent +metadata: + name: sandbox-codex-agent + namespace: demo-magic +spec: + runtime: + type: codex + defaultMaxTurns: 1 + defaultAllowBash: true + model: + name: gpt-5.5 + secretRef: + name: sandbox-model-key +--- +apiVersion: core.orka.ai/v1alpha1 +kind: Task +metadata: + name: orka-live-model-smoke + namespace: demo-magic +spec: + type: agent + agentRef: + name: sandbox-codex-agent + agentRuntime: + maxTurns: 1 + timeout: 10m0s + prompt: "Reply exactly: ORKA_LIVE_MODEL_OK" +YAML + +"$kindctl" kubectl -n demo-magic \ + wait --for=jsonpath='{.status.phase}'=Succeeded task/orka-live-model-smoke --timeout=10m +``` + +- **Installation/configuration parity**: run the model-free CI parity script + below when you want a self-contained cluster bring-up with fake model + credentials. It verifies the install/config path, but it does **not** exercise + claim → ready → exec → cleanup through the direct adapter. + +If you need to demonstrate the intended API shape before RuntimeSession-backed workspace support lands, run it only as an **expected-failure** check and wait for the gate +instead of `Succeeded`: + +```bash +"$kindctl" kubectl apply -f - <<'YAML' +apiVersion: core.orka.ai/v1alpha1 +kind: Agent +metadata: + name: sandbox-codex-agent + namespace: demo-magic +spec: + runtime: + type: codex + defaultMaxTurns: 1 + defaultAllowBash: true + model: + name: gpt-5.5 + secretRef: + name: sandbox-model-key +--- +apiVersion: core.orka.ai/v1alpha1 +kind: Task +metadata: + name: orka-live-sandbox-smoke + namespace: demo-magic +spec: + type: agent + agentRef: + name: sandbox-codex-agent + agentRuntime: + maxTurns: 1 + timeout: 10m0s + execution: + workspace: + enabled: true + templateRef: + name: orka-live-template + reusePolicy: none + cleanupPolicy: delete + prompt: "Reply exactly: ORKA_LIVE_SANDBOX_OK" +YAML + +"$kindctl" kubectl -n demo-magic \ + wait --for=jsonpath='{.status.executionWorkspace.reason}'=WorkspaceValidationFailed \ + task/orka-live-sandbox-smoke --timeout=2m +``` + +Once ACP RuntimeSessions map agent Tasks to execution workspaces, the expected-failure +check can become the live success smoke. At that point, a successful sandbox +wrapper log should include the claimed workspace name, e.g. `completed in +sandbox workspace sandbox-claim-...`. Orka Task status does **not** expose +sandbox claim/exec/cleanup state — read worker logs and upstream agent-sandbox +resources for lifecycle detail. + +### Model-free CI parity + +`scripts/live-agent-sandbox-e2e.sh` (run by the `Live Agent Sandbox E2E` +workflow) stands up a clean kind cluster with fake model credentials and **no +model access**. The script exercises the direct workspace adapter (claim, readiness, router exec, delete, retained reuse, and cleanup) but deliberately skips the unsupported full ACP Task-to-workspace path: + +```bash +bash scripts/live-agent-sandbox-e2e.sh +``` + +That script owns its own cluster lifecycle; do not run it against a kindctl +cluster you want to keep. ## Guardrails diff --git a/.agents/skills/agent-sandbox-deploy/references/validate.md b/.agents/skills/agent-sandbox-deploy/references/validate.md index 836e1d6c3..b63a55317 100644 --- a/.agents/skills/agent-sandbox-deploy/references/validate.md +++ b/.agents/skills/agent-sandbox-deploy/references/validate.md @@ -2,31 +2,14 @@ Validation steps for `$agent-sandbox-deploy`. Read after the standard workflow completes. -> **Known gate (verified live 2026-06): valid enabled provider-based agent -> workspace requests are rejected during execution planning by the current -> service-backed harness runtime.** After workspace validation/resolution, the -> provider-based request documented below fails with -> `status.executionWorkspace.reason=WorkspaceValidationFailed` and message -> `execution workspace is not supported by harness runtime yet`. The gate is in -> `internal/controller/agent_execution_plan.go` (`planAgentExecution`), not a -> misconfiguration. The agent CLI runtimes now -> run through the long-lived `agent-harness-wrapper` service, and the -> Task→sandbox-workspace path for agents is not wired through it yet. A **plain** -> agent Task (no `execution.workspace`) runs fine through the harness + model -> proxy, so use that to confirm the model path. The model-free e2e confirms -> installation/configuration and exercises the direct workspace adapter through -> SandboxClaim readiness, router exec, delete, retained release/reuse, and final -> claim cleanup. It skips only the full execution-workspace Task smoke while the -> harness gate is present. Treat the execution-workspace YAML in the optional -> expected-failure check as the intended future Task API once the harness wires -> workspaces. +> **Known gate:** Orka ACP RuntimeSessions do not yet map to agent-sandbox claims. The direct adapter lifecycle is supported for local validation, but a `Task.spec.execution.workspace` agent Task must still fail closed with `WorkspaceValidationFailed`. Plain Codex/Claude Tasks run through controller-owned ACP RuntimePools and validate the model path separately. Do **not** use an execution-workspace agent Task as the success criterion yet. Validate the three current surfaces separately: installation/configuration, direct workspace-adapter lifecycle, and the model path through a plain agent Task. -- **Model path through the harness** (requires the optional `AGENTIC=1` step and +- **Model path through ACP** (requires the optional `AGENTIC=1` step and vekil ready): run a plain agent Task with no `execution.workspace` and wait for it to succeed. @@ -73,7 +56,7 @@ YAML flags and confirms rollout, then exercises claim → ready → router exec → delete and retained release/reuse → claim cleanup through `AgentSandboxExecutor`. It skips only the full Orka agent Task - workspace path while the harness gate is present. + workspace path while the ACP workspace-dispatch gate is present. If you need to demonstrate the intended API shape before harness workspace support lands, run it only as an **expected-failure** check and wait for the gate @@ -123,7 +106,7 @@ YAML task/orka-live-sandbox-smoke --timeout=2m ``` -Once the harness wires agent Tasks to execution workspaces, the expected-failure +Once ACP RuntimeSessions map agent Tasks to execution workspaces, the expected-failure check can become the live success smoke. At that point, a successful sandbox wrapper log should include the claimed workspace name, e.g. `completed in sandbox workspace sandbox-claim-...`. Orka Task status does **not** expose @@ -140,7 +123,7 @@ smoke that creates SandboxClaims, waits for readiness, executes through the router, deletes one claim, retains and reuses another, and performs final claim cleanup. It skips only the full Orka agent Task workspace smoke, so it proves the provider-adapter path but not Task-to-workspace controller routing, Task status/ -result wiring, harness execution, or model access: +result wiring, ACP Task execution, or model access: ```bash bash scripts/live-agent-sandbox-e2e.sh diff --git a/.agents/skills/agent-substrate-deploy/SKILL.md b/.agents/skills/agent-substrate-deploy/SKILL.md index 36bd86ee3..01cf70b01 100644 --- a/.agents/skills/agent-substrate-deploy/SKILL.md +++ b/.agents/skills/agent-substrate-deploy/SKILL.md @@ -26,8 +26,7 @@ the CI-proven `scripts/agent-substrate-e2e.sh`. **Drive the installer in place; do not copy either script into the skill.** They pin the Substrate revision (`SUBSTRATE_REF`, default `b80031d260959b1fc5c6f61e3099fe2a6d368af1`) and own the heavy lifting: clone Substrate at the pinned ref, create the kind cluster + local -registry, deploy the `ate-system` control plane, build/push the controller, -agent-harness-wrapper, workspace-agent, MCP server, and tool-client images; +registry, deploy the `ate-system` control plane, build/push the controller, workspace-agent, MCP server, and tool-client images; publish the Substrate `ateom-gvisor` image; create a `WorkerPool` + gVisor `ActorTemplate`, initialize the RustFS snapshot bucket, and deploy Orka wired with `--substrate-*`. Re-pin by overriding `SUBSTRATE_REF`, not by editing @@ -120,17 +119,9 @@ is a larger task; confirm scope before attempting it. kind export kubeconfig --name "${cluster}" --kubeconfig "${KUBECONFIG}" DEMO_CLUSTER_REUSE=reuse bash hack/demos/cluster/install-substrate.sh - # The base e2e creates codex-substrate-ci without model env and patches the - # service-backed harness wrapper to use a fake Codex CLI. Patch the Agent with - # the model Secret from the agentic layer and remove the fake CLI override - # before using a plain agent Task as model-validation evidence. - kubectl --context "$ctx" -n default patch agent codex-substrate-ci --type=merge \ - -p "$(jq -cn \ - --arg ref substrate-model-key \ - --arg model gpt-5.5 \ - '{spec:{model:{name:$model},secretRef:{name:$ref}}}')" - kubectl --context "$ctx" -n orka-system set env deployment/orka-agent-harness-wrapper CODEX_CLI_PATH- - kubectl --context "$ctx" -n orka-system rollout status deployment/orka-agent-harness-wrapper --timeout=5m + # This installer validates only the direct Substrate and MCP paths. The + # pre-cutover AGENTIC/model layer is retired. Validate Codex or Claude ACP + # RuntimePools separately with scripts/live-acp-runtime-e2e.sh. ``` > **Login race (verified live 2026-06): disarm vekil's liveness probe before @@ -180,8 +171,85 @@ is a larger task; confirm scope before attempting it. ``` For a model-free validation, stay on `AGENTIC=0` and rely on the built-in - smoke exercises documented in `references/validate.md` instead of standing - up vekil. + smoke exercises (next section) instead of standing up vekil. + +## Validate + +> **Current boundary:** this skill validates direct Substrate Actor and MCP +> behavior only. Orka ACP RuntimeSessions do not yet map to Substrate Actors; +> execution-workspace-backed agent Tasks remain future integration evidence, +> not a success criterion. The removed v1 harness-wrapper path must not be +> reintroduced. Validate plain Codex/Claude ACP Tasks with +> `scripts/live-acp-runtime-e2e.sh` instead. + +The installer leaves a fully wired cluster. During standup it smoke-tests direct +actor create/resume/exec/suspend/delete and Substrate-backed MCP tool lifecycle. +It does **not** currently smoke-test retained workspace reuse for Orka agent +Tasks because ACP RuntimeSession-to-Actor dispatch is not yet wired. + +If you skipped the kubeconfig export in the workflow above, do it before any +manual `kubectl` commands — the e2e standup uses an isolated kubeconfig and does +**not** leave `kind-` in your default one. Keep using the scoped +`KUBECONFIG` in that shell: + +```bash +cluster="${KIND_CLUSTER:-orka-agent-substrate-e2e}" +ctx="kind-${cluster}" +export KUBECONFIG="$(mktemp -t orka-substrate-kubeconfig.XXXXXX)" +kind export kubeconfig --name "${cluster}" --kubeconfig "${KUBECONFIG}" +``` + +To drive an Orka Task yourself (intended shape; currently gated as noted above): + +```bash +cluster="${KIND_CLUSTER:-orka-agent-substrate-e2e}" +ctx="kind-${cluster}" +export KUBECONFIG="$(mktemp -t orka-substrate-kubeconfig.XXXXXX)" +kind export kubeconfig --name "${cluster}" --kubeconfig "${KUBECONFIG}" +kubectl --context "$ctx" -n default apply -f - <<'YAML' +apiVersion: core.orka.ai/v1alpha1 +kind: Task +metadata: + name: substrate-smoke + namespace: default +spec: + type: agent + agentRef: + name: codex-substrate-ci + prompt: "Run make test and summarize the result." + sessionRef: + name: substrate-demo + create: true + execution: + workspace: + enabled: true + provider: substrate + templateRef: + name: orka-codex-ci + namespace: ate-demo + reusePolicy: session + cleanupPolicy: retain +YAML + +kubectl --context "$ctx" -n default get task substrate-smoke -o yaml +``` + +Check the provider-neutral workspace lifecycle in +`status.executionWorkspace` (`phase`, `placement`, `density`, `resumeLatency`). +Status is intentionally sanitized — it must not expose actor IDs, snapshot URIs, +worker pod IPs, daemon URLs, or tokens. + +### CI parity + +`scripts/agent-substrate-e2e.sh` (the `Agent Substrate E2E` workflow) runs the +same path end-to-end and is secret-free. Run it directly when you want a clean, +self-contained validation with its own cluster lifecycle: + +```bash +PATH="$(go env GOPATH)/bin:$PATH" SUBSTRATE_E2E_EXTENDED=1 bash scripts/agent-substrate-e2e.sh +``` + +Set `KEEP_CLUSTER=1` to inspect the cluster after a failure. ## Guardrails diff --git a/.agents/skills/agent-substrate-deploy/references/validate.md b/.agents/skills/agent-substrate-deploy/references/validate.md index c9e48c6e4..a404efdd9 100644 --- a/.agents/skills/agent-substrate-deploy/references/validate.md +++ b/.agents/skills/agent-substrate-deploy/references/validate.md @@ -2,31 +2,12 @@ Validation steps for `$agent-substrate-deploy`. Read after the standard workflow completes. -> **Known gate (verified live 2026-06): valid enabled provider-based agent -> workspace requests are rejected during execution planning by the current -> service-backed harness runtime.** After workspace validation/resolution, a -> `provider: substrate` (or `agent-sandbox`) request fails with -> `status.executionWorkspace.reason=WorkspaceValidationFailed` and message -> `execution workspace is not supported by harness runtime yet`. The gate is in -> `internal/controller/agent_execution_plan.go` (`planAgentExecution`), not a -> misconfiguration — the agent CLI runtimes now -> run through the long-lived `agent-harness-wrapper` service, and the -> Task→workspace path for agents is not wired through it yet. The bundled e2e -> reflects this: it prints `Skipping agent Task execution-workspace checks: -> harness-wrapper runtime is service-backed`. The bundled e2e validates the -> **direct** Substrate path (actor create/resume/router/daemon exec/suspend/delete) -> plus Substrate-backed MCP tool create/reconcile/cleanup. It does not run a plain -> agent Task. After clearing the fake `CODEX_CLI_PATH` override in standard -> workflow step 4 (`Add the model proxy (vekil) — pause for the human`) of -> `../SKILL.md`, use a **plain** agent Task (no `execution.workspace`) to validate -> the harness + model proxy separately. Treat the Task YAML below as the intended -> workspace API once the harness wires workspaces; until then, validate the -> workspace provider via the e2e's direct-actor exercises. +> **Known gate:** Orka ACP RuntimeSessions do not yet map to Substrate Actors. The bundled E2E validates direct Actor create/resume/exec/suspend/delete plus Substrate-backed MCP lifecycle; a provider-backed agent Task remains expected-failure evidence. Validate plain Codex/Claude ACP Tasks separately with `scripts/live-acp-runtime-e2e.sh`. The installer leaves a fully wired cluster. During standup it smoke-tests direct actor create/resume/exec/suspend/delete and Substrate-backed MCP tool lifecycle. It does **not** currently smoke-test retained workspace reuse for Orka agent -Tasks because those execution-workspace checks are skipped by the harness gate. +Tasks because those execution-workspace checks are skipped by the ACP workspace-dispatch gate. If you skipped standard workflow step 3 (`Export kubeconfig for follow-up kubectl commands`) in `../SKILL.md`, do it before any diff --git a/.dockerignore b/.dockerignore index dc21294ef..e4725dbe8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,6 +29,8 @@ # Re-include Go module files !go.mod !go.sum +!LICENSE +!NOTICE.md # Re-include script-local Go helpers used by live E2E Docker builds !scripts/ @@ -55,3 +57,6 @@ ui/tsconfig.tsbuildinfo # Re-include pre-built UI assets for embed !internal/uiembed/dist/ !internal/uiembed/dist/** + +# Re-include the checksum-pinned Codex ACP source patch used by its image build +!workers/acp/images/codex/patch-agent-mode.mjs diff --git a/.github/workflows/agentruntime-external-e2e.yml b/.github/workflows/agentruntime-external-e2e.yml deleted file mode 100644 index 3bc7e4e8b..000000000 --- a/.github/workflows/agentruntime-external-e2e.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: AgentRuntime External Endpoint E2E - -on: - workflow_dispatch: - push: - paths: - - ".dockerignore" - - ".github/workflows/agentruntime-external-e2e.yml" - - "api/**" - - "cmd/**" - - "config/**" - - "examples/harness/**" - - "internal/controller/**" - - "internal/harness/**" - - "test/e2e/**" - - "test/utils/**" - - "Makefile" - - "go.mod" - - "go.sum" - pull_request: - paths: - - ".dockerignore" - - ".github/workflows/agentruntime-external-e2e.yml" - - "api/**" - - "cmd/**" - - "config/**" - - "examples/harness/**" - - "internal/controller/**" - - "internal/harness/**" - - "test/e2e/**" - - "test/utils/**" - - "Makefile" - - "go.mod" - - "go.sum" - -permissions: - contents: read - -concurrency: - group: agentruntime-external-e2e-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - agentruntime-external-e2e: - name: AgentRuntime external endpoint - runs-on: ubuntu-latest - env: - E2E_AGENTRUNTIME_EXTERNAL: "true" - E2E_GO_TEST_TIMEOUT: 25m - KIND_CLUSTER: orka-agentruntime-external-e2e - KIND_VERSION: v0.31.0 - steps: - - name: Clone the code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Free disk space for Docker-heavy E2E - run: | - set -euxo pipefail - echo "Disk usage before cleanup" - df -h - docker system df || true - - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - sudo rm -rf /usr/local/lib/android - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo rm -rf /usr/share/swift - - docker system prune -af || true - docker builder prune -af || true - - echo "Disk usage after cleanup" - df -h - docker system df || true - - - name: Setup Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - - - name: Install kind - run: | - curl -fsSL --retry 3 --retry-delay 2 -o ./kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-$(go env GOARCH)" - chmod +x ./kind - sudo mv ./kind /usr/local/bin/kind - - - name: Verify kind installation - run: kind version - - - name: Run AgentRuntime external endpoint E2E - run: | - set -euxo pipefail - trap 'kind delete cluster --name "${KIND_CLUSTER}" || true' EXIT - make setup-test-e2e KIND_CLUSTER="${KIND_CLUSTER}" - go test -tags=e2e ./test/e2e/ \ - -timeout "${E2E_GO_TEST_TIMEOUT}" \ - -v \ - -ginkgo.v \ - -ginkgo.focus "AgentRuntime external endpoint" diff --git a/.github/workflows/gateway-e2e.yml b/.github/workflows/gateway-e2e.yml index 0081ff5ec..ce376510c 100644 --- a/.github/workflows/gateway-e2e.yml +++ b/.github/workflows/gateway-e2e.yml @@ -50,7 +50,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 env: - E2E_AGENTRUNTIME_EXTERNAL: "true" E2E_EPHEMERAL_CLUSTER: "true" E2E_GATEWAY: "true" E2E_GO_TEST_TIMEOUT: 30m diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml index 9dbf89c47..62955790a 100644 --- a/.github/workflows/helm-chart.yml +++ b/.github/workflows/helm-chart.yml @@ -76,17 +76,16 @@ jobs: exit 1 fi - grep -Fq 'get secret harness-wrapper-auth' config/harness-wrapper/README.md - grep -Fq 'create secret generic harness-wrapper-auth' config/harness-wrapper/README.md - test "$(grep -c 'secretName: harness-wrapper-auth' manifest_staging/deploy/orka.yaml)" -eq 2 - if awk ' - /^---$/ { kind = ""; name = "" } - /^kind: / { kind = $2 } - /^ name: / { name = $2 } - kind == "Secret" && name == "harness-wrapper-auth" { found = 1 } - END { exit(found ? 0 : 1) } - ' manifest_staging/deploy/orka.yaml; then - echo "The raw installer must require a pre-created harness-wrapper-auth Secret, not embed one." >&2 + for secret in acp-artifact-capability workspace-publisher-auth provider-auth-proxy scm-egress-proxy-auth; do + grep -Fq "${secret}" config/acp-workload/README.md + test "$(grep -c "secretName: ${secret}" manifest_staging/deploy/orka.yaml)" -ge 1 + done + if grep -Fq 'harness-wrapper' manifest_staging/deploy/orka.yaml; then + echo "The staged installer reintroduced the removed v1 harness wrapper." >&2 + exit 1 + fi + if grep -q '^kind: Secret$' manifest_staging/deploy/orka.yaml; then + echo "The raw installer must require pre-created ACP Secrets, not embed credentials." >&2 exit 1 fi @@ -100,52 +99,35 @@ jobs: secondary_serviceaccounts=$(mktemp) secondary_gateway_policy=$(mktemp) long_render=$(mktemp) - long_wrapper_service=$(mktemp) - long_wrapper_secret=$(mktemp) - trap 'rm -f "${default_render}" "${secondary_render}" "${secondary_rbac}" "${secondary_serviceaccounts}" "${secondary_gateway_policy}" "${long_render}" "${long_wrapper_service}" "${long_wrapper_secret}"' EXIT - helm template orka manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - > "${default_render}" - helm template secondary manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - > "${secondary_render}" - helm template secondary manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - --show-only templates/rbac.yaml \ - > "${secondary_rbac}" - helm template secondary manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - --show-only templates/serviceaccount.yaml \ - > "${secondary_serviceaccounts}" - helm template secondary manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - --show-only templates/gateway-task-admission-policy.yaml \ - > "${secondary_gateway_policy}" + long_publisher_service=$(mktemp) + long_publisher_secret=$(mktemp) + long_provider_service=$(mktemp) + long_provider_secret=$(mktemp) + long_scm_service=$(mktemp) + long_scm_secret=$(mktemp) + trap 'rm -f "${default_render}" "${secondary_render}" "${secondary_rbac}" "${secondary_serviceaccounts}" "${secondary_gateway_policy}" "${long_render}" "${long_publisher_service}" "${long_publisher_secret}" "${long_provider_service}" "${long_provider_secret}" "${long_scm_service}" "${long_scm_secret}"' EXIT + helm template orka manifest_staging/charts/orka --namespace "${NAMESPACE}" > "${default_render}" + helm template secondary manifest_staging/charts/orka --namespace "${NAMESPACE}" > "${secondary_render}" + helm template secondary manifest_staging/charts/orka --namespace "${NAMESPACE}" --show-only templates/rbac.yaml > "${secondary_rbac}" + helm template secondary manifest_staging/charts/orka --namespace "${NAMESPACE}" --show-only templates/serviceaccount.yaml > "${secondary_serviceaccounts}" + helm template secondary manifest_staging/charts/orka --namespace "${NAMESPACE}" --show-only templates/gateway-task-admission-policy.yaml > "${secondary_gateway_policy}" long_release=$(printf 'r%.0s' {1..53}) - helm template "${long_release}" manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - > "${long_render}" - helm template "${long_release}" manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - --show-only templates/harness-wrapper-service.yaml \ - > "${long_wrapper_service}" - helm template "${long_release}" manifest_staging/charts/orka \ - --namespace "${NAMESPACE}" \ - --set-string workers.harnessWrapper.auth.token=mock-token \ - --show-only templates/harness-wrapper-secret.yaml \ - > "${long_wrapper_secret}" + helm template "${long_release}" manifest_staging/charts/orka --namespace "${NAMESPACE}" > "${long_render}" + helm template "${long_release}" manifest_staging/charts/orka --namespace "${NAMESPACE}" --show-only templates/publisher-service.yaml > "${long_publisher_service}" + helm template "${long_release}" manifest_staging/charts/orka --namespace "${NAMESPACE}" --show-only templates/publisher-secret.yaml > "${long_publisher_secret}" + helm template "${long_release}" manifest_staging/charts/orka --namespace "${NAMESPACE}" --set providerProxy.enabled=true --show-only templates/provider-proxy-service.yaml > "${long_provider_service}" + helm template "${long_release}" manifest_staging/charts/orka --namespace "${NAMESPACE}" --set providerProxy.enabled=true --show-only templates/provider-proxy-secret.yaml > "${long_provider_secret}" + helm template "${long_release}" manifest_staging/charts/orka --namespace "${NAMESPACE}" --show-only templates/scm-egress-proxy-service.yaml > "${long_scm_service}" + helm template "${long_release}" manifest_staging/charts/orka --namespace "${NAMESPACE}" --show-only templates/scm-egress-proxy-secret.yaml > "${long_scm_secret}" grep -Fq -- "--controller-url=http://orka.${NAMESPACE}.svc:8080" "${default_render}" grep -Fq -- "--controller-url=http://secondary-orka.${NAMESPACE}.svc:8080" "${secondary_render}" grep -Fq -- "--ai-worker-service-account-name=orka-ai-worker" "${default_render}" + if grep -Fq -- "namespace: vekil-system" "${default_render}"; then + echo "Default chart values must not require an undeclared external Vekil namespace." >&2 + exit 1 + fi for tier in ai vendor container; do account="secondary-orka-${tier}-worker" grep -Fq -- "--${tier}-worker-service-account-name=${account}" "${secondary_render}" @@ -153,26 +135,30 @@ jobs: grep -Fq -- "name: ${account}" "${secondary_rbac}" grep -Fq -- ":${account}'" "${secondary_gateway_policy}" done - wrapper_name=$(awk '$1 == "name:" { print $2; exit }' "${long_wrapper_service}") - wrapper_secret_name=$(awk '$1 == "name:" { print $2; exit }' "${long_wrapper_secret}") - [[ -n "${wrapper_name}" && ${#wrapper_name} -le 63 ]] - [[ -n "${wrapper_secret_name}" && ${#wrapper_secret_name} -le 63 ]] - grep -Fq -- "http://${wrapper_name}:8080" "${long_render}" - grep -Fq -- "serviceAccountName: ${wrapper_name}" "${long_render}" - grep -Fq -- "secretName: ${wrapper_secret_name}" "${long_render}" + for rendered in "${long_publisher_service}" "${long_publisher_secret}" "${long_provider_service}" "${long_provider_secret}" "${long_scm_service}" "${long_scm_secret}"; do + resource_name=$(awk '$1 == "name:" { print $2; exit }' "${rendered}") + [[ -n "${resource_name}" && ${#resource_name} -le 63 ]] + done + publisher_name=$(awk '$1 == "name:" { print $2; exit }' "${long_publisher_service}") + scm_name=$(awk '$1 == "name:" { print $2; exit }' "${long_scm_service}") + grep -Fq -- "http://${publisher_name}:8080" "${long_render}" + grep -Fq -- "@${scm_name}.${NAMESPACE}.svc:8080" "${long_render}" chart_version=$(awk '$1 == "version:" { print $2; exit }' manifest_staging/charts/orka/Chart.yaml) grep -Fq -- "--general-worker-image=ghcr.io/orka-agents/orka/general-worker:${chart_version}" "${secondary_render}" - for resource in agentruntimes substrateactorpools; do + for resource in agentruntimes branchclaims controllerepochs externaleffects promptattempts publications runtimepools runtimesessioncontrols substrateactorpools; do grep -Eq "resources: \\[.*\"${resource}\"" "${secondary_rbac}" grep -Eq "resources: \\[.*\"${resource}/status\"" "${secondary_rbac}" - grep -Eq "resources: \\[.*\"${resource}/finalizers\"" "${secondary_rbac}" done test "$(grep -Fc 'resources: ["serviceaccounts/token"]' manifest_staging/charts/orka/templates/rbac.yaml)" -eq 1 if grep -Fq 'ai-worker-tokenrequest' manifest_staging/charts/orka/templates/rbac.yaml; then echo "AI workers must not receive an unrestricted TokenRequest ClusterRole." >&2 exit 1 fi + if grep -Rq 'harness-wrapper' manifest_staging/charts/orka/templates; then + echo "The generated chart reintroduced removed v1 harness-wrapper templates." >&2 + exit 1 + fi mkdir -p cr-release-packages helm package manifest_staging/charts/orka --destination cr-release-packages @@ -182,7 +168,7 @@ jobs: echo "Expected exactly one packaged Orka chart, found ${#chart_packages[@]}." >&2 exit 1 fi - test "$(helm show crds "${chart_packages[0]}" | grep -c '^kind: CustomResourceDefinition$')" -eq 19 + test "$(helm show crds "${chart_packages[0]}" | grep -c '^kind: CustomResourceDefinition$')" -eq 26 - name: Install kind run: | @@ -216,19 +202,26 @@ jobs: expected_crds=$(cat <<'CRDS' agentruntimes.core.orka.ai agents.core.orka.ai + branchclaims.core.orka.ai + controllerepochs.core.orka.ai executionworkspaceclasses.workspace.orka.ai executionworkspacepools.workspace.orka.ai executionworkspaceproviders.workspace.orka.ai executionworkspaces.workspace.orka.ai + externaleffects.core.orka.ai fakepoolparameters.fake.workspace.orka.ai fakeproviderconfigs.fake.workspace.orka.ai gatewaybindings.gateway.orka.ai gatewayclasses.gateway.orka.ai gateways.gateway.orka.ai outboundaccesspolicies.core.orka.ai + promptattempts.core.orka.ai providers.core.orka.ai + publications.core.orka.ai repositorymonitors.core.orka.ai repositoryscans.core.orka.ai + runtimepools.core.orka.ai + runtimesessioncontrols.core.orka.ai skills.core.orka.ai substrateactorpools.core.orka.ai tasks.core.orka.ai @@ -266,10 +259,9 @@ jobs: --set controller.image.pullPolicy=IfNotPresent \ --set-string workers.ai.image.repository="${PAUSE_IMAGE_REPOSITORY}" \ --set-string workers.ai.image.tag="${PAUSE_IMAGE_TAG}" \ - --set-string workers.harnessWrapper.image.repository="${PAUSE_IMAGE_REPOSITORY}" \ - --set-string workers.harnessWrapper.image.tag="${PAUSE_IMAGE_TAG}" \ - --set workers.harnessWrapper.image.pullPolicy=IfNotPresent \ - --set-string workers.harnessWrapper.auth.token=mock-token + --set publisher.enabled=false \ + --set providerProxy.enabled=false \ + --set scmEgressProxy.enabled=false assert_all_orka_crds "after helm install" diff --git a/.github/workflows/live-acp-release-gate.yml b/.github/workflows/live-acp-release-gate.yml new file mode 100644 index 000000000..0cdca1a95 --- /dev/null +++ b/.github/workflows/live-acp-release-gate.yml @@ -0,0 +1,180 @@ +name: Live ACP Release Gate + +on: + workflow_dispatch: + inputs: + source_repository: + description: HTTPS GitHub source repository URL used for the release canary + required: true + default: https://github.com/orka-agents/orka.git + type: string + publication_repository: + description: HTTPS URL of a distinct GitHub fork used for publication + required: true + type: string + source_ref: + description: Full SHA of the dispatched workflow commit; must equal the selected base branch head + required: true + type: string + pr_base: + description: Default branch targeted by the temporary canary PR + required: true + default: main + type: string + +permissions: + contents: read + +concurrency: + group: live-acp-release-gate + cancel-in-progress: false + +jobs: + live-acp-release-gate: + name: ACP destructive release acceptance + runs-on: ubuntu-latest + timeout-minutes: 240 + environment: live-acp-release-gate + env: + ACP_E2E_KIND_TAG: acp-release-${{ github.run_id }}-${{ github.run_attempt }} + KIND_VERSION: v0.31.0 + KIND_SHA256_AMD64: eb244cbafcc157dff60cf68693c14c9a75c4e6e6fedaf9cd71c58117cb93e3fa + KIND_SHA256_ARM64: 8e1014e87c34901cc422a1445866835d1e666f2a61301c27e722bdeab5a1f7e4 + steps: + - name: Validate trusted dispatch inputs + env: + CHECKED_OUT_SHA: ${{ github.sha }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + PR_BASE: ${{ inputs.pr_base }} + PUBLICATION_REPOSITORY: ${{ inputs.publication_repository }} + SOURCE_REF: ${{ inputs.source_ref }} + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + run: | + set -Eeuo pipefail + if [[ "${GITHUB_REF}" != "refs/heads/${DEFAULT_BRANCH}" ]]; then + echo "Dispatch this workflow from the default branch (${DEFAULT_BRANCH}) only." >&2 + exit 1 + fi + if [[ "${PR_BASE}" != "${DEFAULT_BRANCH}" ]]; then + echo "pr_base must equal the default branch (${DEFAULT_BRANCH})." >&2 + exit 1 + fi + if [[ ! "${SOURCE_REPOSITORY}" =~ ^https://github\.com/[^/?#]+/[^/?#]+(\.git)?$ ]]; then + echo "source_repository must be an HTTPS github.com repository URL." >&2 + exit 1 + fi + expected_source="https://github.com/${GITHUB_REPOSITORY}" + normalized_source="${SOURCE_REPOSITORY%.git}" + if [[ "${normalized_source,,}" != "${expected_source,,}" ]]; then + echo "source_repository must identify this repository: ${expected_source}.git" >&2 + exit 1 + fi + if [[ ! "${PUBLICATION_REPOSITORY}" =~ ^https://github\.com/[^/?#]+/[^/?#]+(\.git)?$ ]]; then + echo "publication_repository must be an HTTPS github.com repository URL." >&2 + exit 1 + fi + normalized_publication="${PUBLICATION_REPOSITORY%.git}" + if [[ "${normalized_source,,}" == "${normalized_publication,,}" ]]; then + echo "publication_repository must be a distinct fork." >&2 + exit 1 + fi + if [[ ! "${SOURCE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "source_ref must be a full 40-character commit SHA." >&2 + exit 1 + fi + if [[ "${SOURCE_REF,,}" != "${CHECKED_OUT_SHA,,}" ]]; then + echo "source_ref must equal the dispatched workflow commit ${CHECKED_OUT_SHA}." >&2 + exit 1 + fi + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Free disk space for Docker-heavy live E2E + run: | + set -Eeuo pipefail + echo "Disk usage before cleanup" + df -h + docker system df || true + + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/share/swift + + docker system prune -af || true + docker builder prune -af || true + + echo "Disk usage after cleanup" + df -h + docker system df || true + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + + - name: Install kind + run: | + set -Eeuo pipefail + arch="$(go env GOARCH)" + case "${arch}" in + amd64) expected_sha="${KIND_SHA256_AMD64}" ;; + arm64) expected_sha="${KIND_SHA256_ARM64}" ;; + *) echo "unsupported runner architecture: ${arch}" >&2; exit 1 ;; + esac + curl -fsSL --retry 3 --retry-delay 2 -o ./kind \ + "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-${arch}" + printf '%s %s\n' "${expected_sha}" ./kind | sha256sum --check --strict - + chmod +x ./kind + sudo mv ./kind /usr/local/bin/kind + + - name: Verify toolchain + run: | + set -Eeuo pipefail + docker version + git --version + gh --version + jq --version + kind version + kubectl version --client=true + + - name: Run destructive canonical ACP release gate in Kind + env: + ACP_E2E_REF: ${{ inputs.source_ref }} + ACP_E2E_REPO: ${{ inputs.source_repository }} + ACP_E2E_WRITE_CREATE_PR: "1" + ACP_E2E_WRITE_CREDENTIAL_TOKEN: ${{ secrets.ACP_E2E_WRITE_CREDENTIAL_TOKEN }} + ACP_E2E_WRITE_FORGE_CREDENTIAL_TOKEN: ${{ secrets.ACP_E2E_WRITE_FORGE_CREDENTIAL_TOKEN }} + ACP_E2E_WRITE_PR_BASE: ${{ inputs.pr_base }} + ACP_E2E_WRITE_PUBLICATION_REPO: ${{ inputs.publication_repository }} + ACP_E2E_WRITE_READ_CREDENTIAL_TOKEN: ${{ secrets.ACP_E2E_WRITE_READ_CREDENTIAL_TOKEN }} + ACP_E2E_WRITE_SOURCE_REF: ${{ inputs.source_ref }} + ACP_E2E_WRITE_SOURCE_REPO: ${{ inputs.source_repository }} + ACP_E2E_WRITE_TARGET_READ_CREDENTIAL_TOKEN: ${{ secrets.ACP_E2E_WRITE_TARGET_READ_CREDENTIAL_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.ACP_E2E_WRITE_FORGE_CREDENTIAL_TOKEN }} + RELEASE_GATE: "1" + run: | + set -Eeuo pipefail + + required_secrets=( + COPILOT_GITHUB_TOKEN + ACP_E2E_WRITE_READ_CREDENTIAL_TOKEN + ACP_E2E_WRITE_TARGET_READ_CREDENTIAL_TOKEN + ACP_E2E_WRITE_CREDENTIAL_TOKEN + ACP_E2E_WRITE_FORGE_CREDENTIAL_TOKEN + ) + for name in "${required_secrets[@]}"; do + if [[ -z "${!name}" ]]; then + echo "${name} must be configured in the live-acp-release-gate environment." >&2 + exit 1 + fi + done + + bash scripts/live-acp-runtime-kind-e2e.sh diff --git a/.github/workflows/live-acp-runtime-e2e.yml b/.github/workflows/live-acp-runtime-e2e.yml new file mode 100644 index 000000000..1fbd12198 --- /dev/null +++ b/.github/workflows/live-acp-runtime-e2e.yml @@ -0,0 +1,130 @@ +name: Live ACP Runtime E2E + +on: + workflow_dispatch: + schedule: + - cron: "23 8 * * *" + push: + branches: + - main + paths: + - ".dockerignore" + - ".github/workflows/live-acp-runtime-e2e.yml" + - "Dockerfile" + - "Makefile" + - ".agents/skills/kindctl/**" + - ".agents/skills/vekil-reverse-proxy-deploy/**" + - "api/**" + - "cmd/**" + - "config/**" + - "go.mod" + - "go.sum" + - "internal/**" + - "pkg/**" + - "scripts/apply-acp-production.sh" + - "scripts/check-acp-crd-cutover.sh" + - "scripts/lib/kind-local-registry.sh" + - "scripts/lib/live-acp-runtime-kind-bootstrap.sh" + - "scripts/live-acp-runtime-e2e.sh" + - "scripts/live-acp-runtime-kind-e2e.sh" + - "scripts/render-acp-runtime-images.sh" + - "scripts/render-worker-images.sh" + - "workers/**" + +permissions: + contents: read + +concurrency: + group: live-acp-runtime-e2e-${{ github.ref }} + cancel-in-progress: true + +jobs: + live-acp-runtime-e2e: + name: ACP provider smoke + runs-on: ubuntu-latest + timeout-minutes: 180 + environment: live-acp-runtime-smoke + env: + ACP_E2E_REQUIRE_PARALLEL: "1" + ACP_E2E_KIND_TAG: live-acp-${{ github.run_id }}-${{ github.run_attempt }} + KIND_VERSION: v0.31.0 + KIND_SHA256_AMD64: eb244cbafcc157dff60cf68693c14c9a75c4e6e6fedaf9cd71c58117cb93e3fa + KIND_SHA256_ARM64: 8e1014e87c34901cc422a1445866835d1e666f2a61301c27e722bdeab5a1f7e4 + steps: + - name: Validate trusted workflow ref + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -Eeuo pipefail + if [[ "${GITHUB_REF}" != "refs/heads/${DEFAULT_BRANCH}" ]]; then + echo "Run this secret-backed workflow from the default branch (${DEFAULT_BRANCH}) only." >&2 + exit 1 + fi + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Free disk space for Docker-heavy live E2E + run: | + set -Eeuo pipefail + echo "Disk usage before cleanup" + df -h + docker system df || true + + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/share/swift + + docker system prune -af || true + docker builder prune -af || true + + echo "Disk usage after cleanup" + df -h + docker system df || true + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: true + + - name: Install kind + run: | + set -Eeuo pipefail + arch="$(go env GOARCH)" + case "${arch}" in + amd64) expected_sha="${KIND_SHA256_AMD64}" ;; + arm64) expected_sha="${KIND_SHA256_ARM64}" ;; + *) echo "unsupported runner architecture: ${arch}" >&2; exit 1 ;; + esac + curl -fsSL --retry 3 --retry-delay 2 -o ./kind \ + "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-${arch}" + printf '%s %s\n' "${expected_sha}" ./kind | sha256sum --check --strict - + chmod +x ./kind + sudo mv ./kind /usr/local/bin/kind + + - name: Verify toolchain + run: | + set -Eeuo pipefail + docker version + git --version + gh --version + jq --version + kind version + kubectl version --client=true + + - name: Run canonical ACP smoke in Kind + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + run: | + set -Eeuo pipefail + if [[ -z "${COPILOT_GITHUB_TOKEN}" ]]; then + echo "COPILOT_GITHUB_TOKEN must be configured in the live-acp-runtime-smoke environment." >&2 + exit 1 + fi + bash scripts/live-acp-runtime-kind-e2e.sh diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index bb3e915dd..bd712c887 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -100,7 +100,8 @@ jobs: helm lint cmd/build/helmify/static helm lint charts/orka - test "$(helm show crds charts/orka | grep -c '^kind: CustomResourceDefinition$')" -eq 19 + expected_crds=$(find config/crd/bases -maxdepth 1 -type f -name '*.yaml' | wc -l | tr -d ' ') + test "$(helm show crds charts/orka | grep -c '^kind: CustomResourceDefinition$')" -eq "${expected_crds}" git diff --check - name: Create release pull request diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22babf2b7..5dc8b9804 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,13 +70,13 @@ jobs: diff --no-dereference --recursive --unified manifest_staging/charts/orka charts/orka diff --no-dereference --recursive --unified manifest_staging/deploy deploy helm lint charts/orka - test "$(helm show crds charts/orka | grep -c '^kind: CustomResourceDefinition$')" -eq 19 + expected_crds=$(find config/crd/bases -maxdepth 1 -type f -name '*.yaml' | wc -l | tr -d ' ') + test "$(helm show crds charts/orka | grep -c '^kind: CustomResourceDefinition$')" -eq "${expected_crds}" rendered_chart=$(mktemp) trap 'rm -f "${rendered_chart}"' EXIT helm template release-validation charts/orka \ --namespace orka-system \ - --set-string workers.harnessWrapper.auth.token=mock-token \ > "${rendered_chart}" runtime_images=() @@ -85,9 +85,9 @@ jobs: image_ref=${image_ref#\"} image_ref=${image_ref%\"} runtime_images+=("${image_ref}") - done < <(grep -E '^[[:space:]]+image:[[:space:]]+' "${rendered_chart}") + done < <(grep -E '^[[:space:]]+image:[[:space:]]+' "${rendered_chart}" | sort -u) if [[ ${#runtime_images[@]} -ne 2 ]]; then - echo "Expected two rendered runtime images; found ${#runtime_images[@]}." >&2 + echo "Expected two unique rendered controller/publisher images; found ${#runtime_images[@]}." >&2 exit 1 fi for image_ref in "${runtime_images[@]}"; do @@ -121,8 +121,9 @@ jobs: echo "Expected exactly one packaged Orka chart, found ${#chart_packages[@]}." >&2 exit 1 fi - if [[ $(helm show crds "${chart_packages[0]}" | grep -c '^kind: CustomResourceDefinition$') -ne 19 ]]; then - echo "Packaged Orka chart must contain exactly 19 CRDs." >&2 + expected_crds=$(find config/crd/bases -maxdepth 1 -type f -name '*.yaml' | wc -l | tr -d ' ') + if [[ $(helm show crds "${chart_packages[0]}" | grep -c '^kind: CustomResourceDefinition$') -ne ${expected_crds} ]]; then + echo "Packaged Orka chart CRD count does not match config/crd/bases (${expected_crds})." >&2 exit 1 fi @@ -218,9 +219,21 @@ jobs: image_suffix: "/general-worker" dockerfile: workers/general/Dockerfile context: . - - image: agent-harness-wrapper - image_suffix: "/agent-harness-wrapper" - dockerfile: workers/harness/Dockerfile + - image: acp-codex-runtime + image_suffix: "/acp-codex-runtime" + dockerfile: workers/acp/images/codex/Dockerfile + context: . + - image: acp-claude-runtime + image_suffix: "/acp-claude-runtime" + dockerfile: workers/acp/images/claude/Dockerfile + context: . + - image: acp-copilot-runtime + image_suffix: "/acp-copilot-runtime" + dockerfile: workers/acp/images/copilot/Dockerfile + context: . + - image: workspace-publisher + image_suffix: "/workspace-publisher" + dockerfile: workers/publisher/Dockerfile context: . steps: - name: Checkout @@ -336,12 +349,36 @@ jobs: image_suffix: "/general-worker" platform: linux/arm64 platform_slug: linux-arm64 - - image: agent-harness-wrapper - image_suffix: "/agent-harness-wrapper" + - image: acp-codex-runtime + image_suffix: "/acp-codex-runtime" + platform: linux/amd64 + platform_slug: linux-amd64 + - image: acp-codex-runtime + image_suffix: "/acp-codex-runtime" + platform: linux/arm64 + platform_slug: linux-arm64 + - image: acp-claude-runtime + image_suffix: "/acp-claude-runtime" + platform: linux/amd64 + platform_slug: linux-amd64 + - image: acp-claude-runtime + image_suffix: "/acp-claude-runtime" + platform: linux/arm64 + platform_slug: linux-arm64 + - image: acp-copilot-runtime + image_suffix: "/acp-copilot-runtime" + platform: linux/amd64 + platform_slug: linux-amd64 + - image: acp-copilot-runtime + image_suffix: "/acp-copilot-runtime" + platform: linux/arm64 + platform_slug: linux-arm64 + - image: workspace-publisher + image_suffix: "/workspace-publisher" platform: linux/amd64 platform_slug: linux-amd64 - - image: agent-harness-wrapper - image_suffix: "/agent-harness-wrapper" + - image: workspace-publisher + image_suffix: "/workspace-publisher" platform: linux/arm64 platform_slug: linux-arm64 steps: @@ -400,8 +437,14 @@ jobs: image_suffix: "/ai-worker" - image: general-worker image_suffix: "/general-worker" - - image: agent-harness-wrapper - image_suffix: "/agent-harness-wrapper" + - image: acp-codex-runtime + image_suffix: "/acp-codex-runtime" + - image: acp-claude-runtime + image_suffix: "/acp-claude-runtime" + - image: acp-copilot-runtime + image_suffix: "/acp-copilot-runtime" + - image: workspace-publisher + image_suffix: "/workspace-publisher" steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 @@ -519,7 +562,10 @@ jobs: promote_image controller "" promote_image ai-worker "/ai-worker" promote_image general-worker "/general-worker" - promote_image agent-harness-wrapper "/agent-harness-wrapper" + promote_image acp-codex-runtime "/acp-codex-runtime" + promote_image acp-claude-runtime "/acp-claude-runtime" + promote_image acp-copilot-runtime "/acp-copilot-runtime" + promote_image workspace-publisher "/workspace-publisher" publish-helm-chart: diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 7d01ebc6d..9316eecda 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -25,6 +25,7 @@ jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest + timeout-minutes: 60 env: KIND_VERSION: v0.31.0 steps: diff --git a/.golangci.yml b/.golangci.yml index a8e84c1f3..d93d39b5c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,6 +40,18 @@ linters: - dupl - lll path: internal/* + - linters: + - lll + path: workers/acp/supervisor/* + - linters: + - lll + path: cmd/cli/* + - linters: + - lll + path: cmd/orka-provider-auth-proxy/* + - linters: + - lll + path: cmd/main.go paths: - third_party$ - builtin$ diff --git a/AGENTS.md b/AGENTS.md index a32115828..b498f320e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,94 +13,86 @@ Orka is a Kubernetes-native task execution platform that manages Jobs and Pods f ## Generated — do not edit -| Path | Regenerate with | -| --- | --- | -| `config/crd/bases/*.yaml`, `config/rbac/role.yaml` | `make manifests` | -| `manifest_staging/deploy/orka.yaml`, `manifest_staging/charts/orka/**` | `make manifests` | -| `deploy/**`, `charts/orka/**` | `make promote-staging-manifest` (release-preparation flow only) | -| `**/zz_generated.*.go` | `make generate` | -| `PROJECT` | kubebuilder CLI | -| `ui/src/routeTree.gen.ts` | TanStack Router | +For non-trivial code changes, run `$autoreview` (`.agents/skills/autoreview/SKILL.md`) before final/commit/ship and keep going until there are no accepted/actionable findings, unless the change is trivial/docs-only, equivalent manual review already happened, or the human opts out. -## Gotchas - -Execution model: +- Treat review output as advisory: verify every finding against the real code path before changing code. +- If review-triggered fixes change code, rerun focused tests and rerun `$autoreview`. +- Format before review when formatting can move line locations; focused tests and review may run in parallel only after formatting is stable. -- `runtimeRef` AgentRuntime tasks are remote-runtime tasks — there is no Kubernetes Job/Pod per task. Orka stays the governance plane, brokered tools execute through Orka, and remote adapters receive only harness auth plus safe tool schemas, never downstream production tool credentials. -- Agent CLI runtimes (`codex`, `claude`, `copilot`, `opencode`) run through the `agent-harness-wrapper`. The old per-runtime worker images and entrypoints are gone. -- Harness-wrapper success maps `TurnCompleted` to `AgentRuntimeCompleted` plus terminal task events. Do not expect a worker `ResultSubmitted` event on harness-backed agent tasks. -- Harness wrapper `GET /v1/health` and `GET /v1/capabilities` are intentionally unauthenticated; mutating turn endpoints (`POST /v1/turns`, cancel) require the wrapper bearer token. -- The harness wrapper may emit restricted PodSecurity warnings — it runs as root with limited capabilities for child process/credential setup. Rollout success plus runtime live tests are the source of truth. -- When changing the harness wrapper, run the canonical [live validation checklist](website/docs/guides/cli-harness-wrapper.md#live-validation-checklist). +## PR Closeout -Workers: +After creating or updating an agent-authored PR, use `$pr-closeout` (`.agents/skills/pr-closeout/SKILL.md`) by default, like `$autoreview` is used before landing. Resolve merge conflicts, fix failing CI, address or push back on unresolved review threads, reply on GitHub and resolve addressed comments, push the non-main PR branch, and repeat until current CI is green and no unresolved actionable review threads remain. Skip only when the human opts out, the PR is intentionally draft/WIP, or the remaining blocker is external/human-only. Do not merge or enable auto-merge unless explicitly asked. -- Worker filesystem is read-only except `/tmp`, `/home/worker`, and `/workspace`. -- AI worker truncates messages on context overflow — keeps system prompt plus newest, drops the middle atomically with structured metadata. -- Built-in AI worker tools: `web_search`, `code_exec`, `file_read`, `web_fetch`, `file_write`. +## Build & Test -Memory and coordination: - -- Coordination memory tools: `recall_memory`, `remember`, `propose_memory`, `search_transcript`. -- Memory is governance-first: `remember` and `propose_memory` create review proposals, not durable memories. -- Reviewing a memory proposal does not apply it. Use the explicit proposal apply endpoint for accepted `memory` proposals when durable memory should be created. -- Never put secrets, credentials, tokens, raw transcripts, or one-off task status in durable memory. +```bash +make manifests # Regenerate CRDs (after editing *_types.go or markers) +make generate # Regenerate Go types +make build # Build (includes UI) +make test # Run tests +make lint-fix # Lint and fix +make docker-build-all # Controller, AI/general workers, ACP runtimes, publisher +make deploy IMG=@sha256: ACP_CODEX_RUNTIME_IMG=@sha256: ACP_CLAUDE_RUNTIME_IMG=@sha256: ACP_COPILOT_RUNTIME_IMG=@sha256: WORKSPACE_PUBLISHER_IMG=@sha256: +``` -Auth and telemetry: +UI: `cd ui && bun install && bun run dev` (dev server on :5173). See @website/docs/development/development.md for full commands. -- Transaction tokens are accepted via `Txn-Token` by default. `Authorization: Bearer` context-token support is opt-in so ServiceAccount/OIDC auth can coexist. -- Live GitHub OIDC E2E requires GitHub Actions `id-token: write` or `ORKA_GITHUB_OIDC_TOKEN`. Redact JWTs, TxTokens, and request tokens in logs. -- OpenTelemetry GenAI constants are hand-rolled in `internal/tracing/genai` because the GenAI conventions are still Development-stage. Telemetry is enabled with `--enable-telemetry`/`--enable-tracing`, workers honor `ORKA_ENABLE_TELEMETRY`, and prompt/completion content capture stays default-off and fail-closed. +For testing against a local Kubernetes cluster, use the `$kindctl` skill to manage repo/worktree-scoped kind clusters without touching the global kubeconfig. -Build: +To stand up a reverse proxy for Anthropic/Gemini/OpenAI-compatible clients, use the `$vekil-reverse-proxy-deploy` skill. When it falls back to GitHub Copilot device-code login, surface the login code and URL to the user and wait for their confirmation before continuing — never complete the login on their behalf. -- `make build` requires UI assets — run `make ui-build` first, or `ensure-ui-embed` creates a stub and the embedded UI won't work. +To stand up an execution-workspace provider on a local kind cluster for evaluation, use the `$agent-sandbox-deploy` skill (kubernetes-sigs agent-sandbox; pairs with `$kindctl` for the cluster and `$orka-kind-deploy` for the controller) or the `$agent-substrate-deploy` skill (Agent Substrate; owns its own gVisor kind cluster, so it is not hosted on a `$kindctl` cluster). Both are local/kind eval only — Orka does not install or manage these providers in production — and both surface the `$vekil-reverse-proxy-deploy` device-code login to the user for confirmation rather than completing it. -Helm manifests and release snapshots: +## Verification -- Helm generator inputs live under `cmd/build/helmify/`; canonical Kubernetes inputs remain under `config/`. -- `make manifests` regenerates the committed next-release outputs in `manifest_staging/deploy/orka.yaml` and `manifest_staging/charts/orka/`. Edit the source inputs, not generated staging files, and commit both source and regenerated output. -- Root `deploy/` and `charts/orka/` are promoted release snapshots. Do not edit them directly; only the release-preparation flow runs `make release-manifest` and `make promote-staging-manifest`. Staging may intentionally be ahead of the root snapshots. -- A pushed `v*` tag packages and publishes the already-reviewed root snapshot. Tag publication must not regenerate or promote manifests. -- Chart CRDs are generated from `config/crd/bases/`. Helm does not update them during `helm upgrade`; apply the CRDs from the exact target chart before upgrading the release. +Run after every change: -## Code style +```bash +make manifests generate # After *_types.go or marker edits +make lint-fix && make test # After any *.go edits +cd ui && bun run lint && bun run test # After UI edits +bash -n scripts/*.sh # After shell script edits +go run github.com/rhysd/actionlint/cmd/actionlint@latest .github/workflows/.yml # After workflow edits +``` -- Structured logging: `log := log.FromContext(ctx); log.Info("msg", "key", val)` -- LLM tool args for nested objects arrive as `map[string]any`, not strings — always type-switch. -- Put model-readable tool constraints in the JSON Schema (`maximum`, `minimum`, `enum`, `default`), not only in description prose, and validate and enforce them at runtime in `Execute`; schema guides the model but is not a runtime trust boundary. +Single test: `go test ./internal/api/ -run TestHandlerName -v` -## Build and verify +## Auto-Generated — Do NOT Edit -```bash -make manifests # After CRD/RBAC/Kustomize or Helm generator input changes -make generate # After generated Go type input changes -make lint-fix && make test # After any *.go edits -make build # Includes UI; see ui-build gotcha above -make docker-build-all # Controller, AI/general workers, harness wrapper -make deploy IMG=/orka:tag HARNESS_WRAPPER_IMG=/agent-harness-wrapper:tag -``` +- `config/crd/bases/*.yaml`, `config/rbac/role.yaml` — `make manifests` +- `manifest_staging/deploy/orka.yaml`, `manifest_staging/charts/orka/**` — `make manifests` +- `deploy/**`, `charts/orka/**` — `make promote-staging-manifest` (release-preparation only) +- `**/zz_generated.*.go` — `make generate` +- `PROJECT` — kubebuilder CLI +- `ui/src/routeTree.gen.ts` — TanStack Router -```bash -cd ui && bun run lint && bun run test # After UI edits -bash -n scripts/*.sh # After shell script edits -go run github.com/rhysd/actionlint/cmd/actionlint@latest .github/workflows/.yml -``` +Do NOT delete `// +kubebuilder:scaffold:*` comments. -Single Go test: `go test ./internal/api/ -run TestHandlerName -v`. -UI dev server: `cd ui && bun install && bun run dev` (:5173). +## Code Style -Full command reference, CI workflow catalog, and OpenTelemetry development notes: -`website/docs/development/development.md`. +- Structured logging: `log := log.FromContext(ctx); log.Info("msg", "key", val)` +- LLM tool args for nested objects arrive as `map[string]any`, not strings — always type-switch +- Put model-readable tool constraints in JSON Schema (`maximum`, `minimum`, `enum`, `default`), then validate and enforce them again in `Execute`; schema is guidance, not a runtime trust boundary +- Memory features are governance-first: `remember` and `propose_memory` create review proposals, not durable memories +- Kontxt integration is fail-closed: never store raw TxTokens in Task specs/status/logs; use owner-referenced Secrets for child tokens, safe metadata/digests for audit, subset checks for child scopes, and fail-closed TTS exchanges for outbound scopes. -## Skills +## Gotchas -| Skill | Use for | -| --- | --- | -| `$autoreview` | Review before commit/land on non-trivial code changes. Repeat until no accepted/actionable findings remain. Skip for trivial/docs-only work, equivalent manual review, or when the human opts out. | -| `$pr-closeout` | After creating or updating an agent-authored PR, drive it to green. Skip when the human opts out, the PR is intentionally draft/WIP, or the blocker is external/human-only. | -| `$kindctl` | Repo/worktree-scoped kind clusters, without touching the global kubeconfig. | -| `$orka-kind-deploy` | Rebuild and redeploy the full local stack into a kind cluster. | -| `$vekil-reverse-proxy-deploy` | Reverse proxy for Anthropic/Gemini/OpenAI-compatible clients. | -| `$agent-sandbox-deploy` | kubernetes-sigs agent-sandbox workspace provider (local/kind eval only). | -| `$agent-substrate-deploy` | Agent Substrate workspace provider (local/kind eval only; owns its own cluster). | +- Worker filesystem is read-only except `/tmp`, `/home/worker`, and `/workspace` +- `make build` requires UI assets — run `make ui-build` first (or `ensure-ui-embed` creates a stub) +- AI worker truncates messages on context overflow — keeps system prompt + newest, drops middle atomically with structured metadata +- `code_exec` timeout max is 60s — values above are ignored (30s default used) +- Built-in AI worker tools: `web_search`, `code_exec`, `file_read`, `web_fetch`, `file_write` +- Built-in agent runtimes (`codex`, `claude`, `copilot`) use only the `orka.harness.v2` ACP RuntimePool path; there is no per-Task Job or legacy fallback. +- `Task.spec.workspace` is the only agent repository surface. Keep clone/read credentials in `readCredentialRef` and publication/forge credentials in `publicationCredentialRef`; neither enters the ACP process tree. +- RuntimePools are controller-owned, digest-pinned, scale-to-zero resources. Only `Serving` + `Accepting` admits new RuntimeSessions; drain/finalization must complete before replacement or scale-down. +- Safe v2 probes are `GET /v2/health` and `GET /v2/capabilities`; status and all mutations require controller authentication plus operation-scoped authorization and exact fences. +- External `runtimeRef` registrations are v2-only. Registration/conformance exists, but external Task dispatch currently fails closed until its v2 dispatcher is wired. +- ACP runtime Pods run the supervisor as root with narrowly added process/identity capabilities; ACP children use distinct non-reused UIDs/GIDs, private session trees, and no Git credentials. +- Coordination memory tools: `recall_memory`, `remember`, `propose_memory`, `search_transcript` +- Do not store secrets, credentials, tokens, raw transcripts, or one-off task status in durable memory +- Reviewing a memory proposal does not apply it; use the explicit proposal apply endpoint for accepted `memory` proposals when durable memory should be created +- Kontxt TxTokens are accepted via `Txn-Token` by default; `Authorization: Bearer` context-token support is opt-in so ServiceAccount/OIDC auth can coexist +- Live GitHub OIDC/kontxt E2E requires GitHub Actions `id-token: write` or `ORKA_GITHUB_OIDC_TOKEN`; redact JWTs, TxTokens, and request tokens in logs +- OpenTelemetry GenAI constants are hand-rolled in `internal/tracing/genai`; telemetry is enabled with `--enable-telemetry`/`--enable-tracing`, workers honor `ORKA_ENABLE_TELEMETRY`, and prompt/completion content capture remains default-off/fail-closed +- ACP real-world validation should include Codex and Claude through Vekil, Copilot image/profile admission (plus live execution when provider auth is available), workspace clone/read, Session continuation, cancellation/timeout, unsafe workspace rejection, controller restart, pool replacement, clean-room branch publication, PR reconciliation, and cleanup. diff --git a/Dockerfile b/Dockerfile index 3a277801a..da778cbb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ +# syntax=docker/dockerfile:1.7.1@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e + # Build the UI -FROM oven/bun:1 AS ui-builder +FROM --platform=$BUILDPLATFORM docker.io/oven/bun:1@sha256:e10577f0db68676a7024391c6e5cb4b879ebd17188ab750cf10024a6d700e5c4 AS ui-builder WORKDIR /app COPY ui/package.json ui/bun.lock ./ RUN bun install --frozen-lockfile @@ -7,7 +9,7 @@ COPY ui/ . RUN bun run build # Build the manager binary -FROM golang:1.26 AS builder +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26@sha256:3aff6657219a4d9c14e27fb1d8976c49c29fddb70ba835014f477e1c70636647 AS builder ARG TARGETOS ARG TARGETARCH @@ -30,13 +32,17 @@ COPY --from=ui-builder /app/dist/ internal/uiembed/dist/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager ./cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager ./cmd/main.go \ + && CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o provider-auth-proxy ./cmd/orka-provider-auth-proxy \ + && CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o scm-egress-proxy ./cmd/orka-scm-egress-proxy # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot +FROM gcr.io/distroless/static:nonroot@sha256:f7f8f729987ad0fdf6b05eeeae94b26e6a0f613bdf46feea7fc40f7bd72953e6 WORKDIR / COPY --from=builder /workspace/manager . +COPY --from=builder /workspace/provider-auth-proxy . +COPY --from=builder /workspace/scm-egress-proxy . USER 65532:65532 ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index c5298fe4f..7b28a6f2f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,10 @@ VERSION := v0.1.1 IMG ?= controller:latest AI_WORKER_IMG ?= ghcr.io/orka-agents/orka/ai-worker:latest GENERAL_WORKER_IMG ?= ghcr.io/orka-agents/orka/general-worker:latest -HARNESS_WRAPPER_IMG ?= ghcr.io/orka-agents/orka/agent-harness-wrapper:latest +ACP_CODEX_RUNTIME_IMG ?= ghcr.io/orka-agents/orka/acp-codex-runtime:latest +ACP_CLAUDE_RUNTIME_IMG ?= ghcr.io/orka-agents/orka/acp-claude-runtime:latest +ACP_COPILOT_RUNTIME_IMG ?= ghcr.io/orka-agents/orka/acp-copilot-runtime:latest +WORKSPACE_PUBLISHER_IMG ?= ghcr.io/orka-agents/orka/workspace-publisher:latest # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -64,7 +67,7 @@ manifests: controller-gen kustomize ## Generate canonical and Gatekeeper-style s }; \ trap cleanup EXIT; \ mkdir -p "$$tmp/deploy" "$$tmp/charts/orka"; \ - "$(KUSTOMIZE)" build config/default -o "$$tmp/deploy/orka.yaml"; \ + "$(KUSTOMIZE)" build config/acp-production -o "$$tmp/deploy/orka.yaml"; \ "$(KUSTOMIZE)" build \ --load-restrictor LoadRestrictionsNone \ cmd/build/helmify | go run ./cmd/build/helmify -output-dir "$$tmp/charts/orka"; \ @@ -113,6 +116,19 @@ promote-staging-manifest: ## Promote committed staging manifests into release sn trap - EXIT; \ rm -rf "$$stage" "$$backup" +.PHONY: sync-helm-crds +sync-helm-crds: ## Synchronize generated CRDs into the promoted Helm chart while preserving non-CRD files. + scripts/sync-helm-crds.sh + +.PHONY: verify-helm-crds +verify-helm-crds: ## Verify generated and promoted Helm chart CRDs are identical. + scripts/sync-helm-crds.sh --check + +.PHONY: test-helm-crd-sync +test-helm-crd-sync: ## Test Helm CRD synchronization and drift detection. + bash scripts/tests/sync-helm-crds-test.sh + + .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..." @@ -155,7 +171,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests. # The default e2e setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. KIND_CLUSTER ?= orka-test-e2e -E2E_GO_TEST_TIMEOUT ?= 30m +E2E_GO_TEST_TIMEOUT ?= 50m E2E_GINKGO_FOCUS ?= E2E_GINKGO_FOCUS_ARG = $(if $(E2E_GINKGO_FOCUS),-ginkgo.focus="$(E2E_GINKGO_FOCUS)",) @@ -188,7 +204,10 @@ test-e2e-setup-only: setup-test-e2e docker-build-all ## Set up Kind cluster and $(KIND) load docker-image $(IMG) --name $(KIND_CLUSTER) $(KIND) load docker-image $(AI_WORKER_IMG) --name $(KIND_CLUSTER) $(KIND) load docker-image $(GENERAL_WORKER_IMG) --name $(KIND_CLUSTER) - $(KIND) load docker-image $(HARNESS_WRAPPER_IMG) --name $(KIND_CLUSTER) + $(KIND) load docker-image $(ACP_CODEX_RUNTIME_IMG) --name $(KIND_CLUSTER) + $(KIND) load docker-image $(ACP_CLAUDE_RUNTIME_IMG) --name $(KIND_CLUSTER) + $(KIND) load docker-image $(ACP_COPILOT_RUNTIME_IMG) --name $(KIND_CLUSTER) + $(KIND) load docker-image $(WORKSPACE_PUBLISHER_IMG) --name $(KIND_CLUSTER) .PHONY: test-e2e-run-only test-e2e-run-only: manifests generate fmt vet ## Run e2e tests without rebuilding images (for fast iteration). @@ -314,31 +333,55 @@ docker-push: ## Push docker image with the manager. docker-build-ai-worker: ## Build docker image for the AI worker. $(CONTAINER_TOOL) build -t ${AI_WORKER_IMG} -f workers/ai/Dockerfile . -.PHONY: docker-build-harness-wrapper -docker-build-harness-wrapper: ## Build docker image for the agent harness wrapper. - $(CONTAINER_TOOL) build -t ${HARNESS_WRAPPER_IMG} -f workers/harness/Dockerfile . - .PHONY: docker-build-general-worker docker-build-general-worker: ## Build docker image for the general worker. $(CONTAINER_TOOL) build -t ${GENERAL_WORKER_IMG} -f workers/general/Dockerfile . +.PHONY: docker-build-acp-codex-runtime +docker-build-acp-codex-runtime: ## Build the immutable Codex ACP runtime image. + $(CONTAINER_TOOL) build -t ${ACP_CODEX_RUNTIME_IMG} -f workers/acp/images/codex/Dockerfile . + +.PHONY: docker-build-acp-claude-runtime +docker-build-acp-claude-runtime: ## Build the immutable Claude ACP runtime image. + $(CONTAINER_TOOL) build -t ${ACP_CLAUDE_RUNTIME_IMG} -f workers/acp/images/claude/Dockerfile . + +.PHONY: docker-build-acp-copilot-runtime +docker-build-acp-copilot-runtime: ## Build the immutable GitHub Copilot ACP runtime image. + $(CONTAINER_TOOL) build -t ${ACP_COPILOT_RUNTIME_IMG} -f workers/acp/images/copilot/Dockerfile . + +.PHONY: docker-build-workspace-publisher +docker-build-workspace-publisher: ## Build the clean-room workspace publisher image. + $(CONTAINER_TOOL) build -t ${WORKSPACE_PUBLISHER_IMG} -f workers/publisher/Dockerfile . + .PHONY: docker-push-ai-worker docker-push-ai-worker: ## Push docker image for the AI worker. $(CONTAINER_TOOL) push ${AI_WORKER_IMG} -.PHONY: docker-push-harness-wrapper -docker-push-harness-wrapper: ## Push docker image for the agent harness wrapper. - $(CONTAINER_TOOL) push ${HARNESS_WRAPPER_IMG} - .PHONY: docker-push-general-worker docker-push-general-worker: ## Push docker image for the general worker. $(CONTAINER_TOOL) push ${GENERAL_WORKER_IMG} +.PHONY: docker-push-acp-codex-runtime +docker-push-acp-codex-runtime: ## Push the immutable Codex ACP runtime image. + $(CONTAINER_TOOL) push ${ACP_CODEX_RUNTIME_IMG} + +.PHONY: docker-push-acp-claude-runtime +docker-push-acp-claude-runtime: ## Push the immutable Claude ACP runtime image. + $(CONTAINER_TOOL) push ${ACP_CLAUDE_RUNTIME_IMG} + +.PHONY: docker-push-acp-copilot-runtime +docker-push-acp-copilot-runtime: ## Push the immutable GitHub Copilot ACP runtime image. + $(CONTAINER_TOOL) push ${ACP_COPILOT_RUNTIME_IMG} + +.PHONY: docker-push-workspace-publisher +docker-push-workspace-publisher: ## Push the clean-room workspace publisher image. + $(CONTAINER_TOOL) push ${WORKSPACE_PUBLISHER_IMG} + .PHONY: docker-build-all -docker-build-all: docker-build docker-build-ai-worker docker-build-general-worker docker-build-harness-wrapper ## Build all docker images. +docker-build-all: docker-build docker-build-ai-worker docker-build-general-worker docker-build-acp-codex-runtime docker-build-acp-claude-runtime docker-build-acp-copilot-runtime docker-build-workspace-publisher ## Build all docker images. .PHONY: docker-push-all -docker-push-all: docker-push docker-push-ai-worker docker-push-general-worker docker-push-harness-wrapper ## Push all docker images. +docker-push-all: docker-push docker-push-ai-worker docker-push-general-worker docker-push-acp-codex-runtime docker-push-acp-claude-runtime docker-push-acp-copilot-runtime docker-push-workspace-publisher ## Push all docker images. ##@ Deployment @@ -356,24 +399,70 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi +.PHONY: verify-acp-runtime-images +verify-acp-runtime-images: ## Require digest-pinned ACP runtime images for supported deployments. + @for entry in \ + "IMG=$(IMG)" \ + "WORKSPACE_PUBLISHER_IMG=$(WORKSPACE_PUBLISHER_IMG)" \ + "ACP_CODEX_RUNTIME_IMG=$(ACP_CODEX_RUNTIME_IMG)" \ + "ACP_CLAUDE_RUNTIME_IMG=$(ACP_CLAUDE_RUNTIME_IMG)" \ + "ACP_COPILOT_RUNTIME_IMG=$(ACP_COPILOT_RUNTIME_IMG)"; do \ + name="$${entry%%=*}"; ref="$${entry#*=}"; \ + if [[ ! "$${ref}" =~ ^.+@sha256:[0-9a-f]{64}$$ ]]; then \ + echo "$$name must be an immutable image reference ending in @sha256:<64 lowercase hex characters>; got '$$ref'" >&2; \ + exit 1; \ + fi; \ + done + +.PHONY: verify-acp-crd-cutover +verify-acp-crd-cutover: ## Refuse workload deployment until the ACP v2 hard cutover is complete. + @for crd in runtimepools promptattempts runtimesessioncontrols branchclaims publications controllerepochs externaleffects; do \ + "$(KUBECTL)" get crd "$$crd.core.orka.ai" >/dev/null || { echo "missing ACP control CRD: $$crd.core.orka.ai; run scripts/upgrade-orka-crds.sh first" >&2; exit 1; }; \ + done + @"$(KUBECTL)" get crd agentruntimes.core.orka.ai -o json | jq -e \ + '[.spec.versions[] | select(.served == true) | .schema.openAPIV3Schema.properties.spec.properties.contractVersion.enum] | all(. == ["orka.harness.v2"])' >/dev/null || \ + { echo "AgentRuntime CRD is not the ACP v2-only schema; run scripts/upgrade-orka-crds.sh" >&2; exit 1; } + @if "$(KUBECTL)" get agentruntimes.core.orka.ai -A -o json | jq -e 'any(.items[]?; .spec.contractVersion == "orka.harness.v1")' >/dev/null; then \ + echo "legacy orka.harness.v1 AgentRuntime objects remain" >&2; exit 1; \ + fi + @KUBECTL="$(KUBECTL)" scripts/check-legacy-wrapper-resources.sh + .PHONY: deploy -deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} - cd config/harness-wrapper && "$(KUSTOMIZE)" edit set image ghcr.io/orka-agents/orka/agent-harness-wrapper=${HARNESS_WRAPPER_IMG} +deploy: verify-acp-runtime-images verify-acp-crd-cutover manifests kustomize ## Deploy ACP workloads after verified CRD hard cutover. @"$(KUBECTL)" create namespace orka-system --dry-run=client -o yaml | "$(KUBECTL)" apply -f - - @if ! "$(KUBECTL)" -n orka-system get secret harness-wrapper-auth >/dev/null 2>&1; then \ + @if ! "$(KUBECTL)" -n orka-system get secret acp-artifact-capability >/dev/null 2>&1; then \ + secret="$$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n')"; \ + "$(KUBECTL)" -n orka-system create secret generic acp-artifact-capability --from-literal=capability-secret="$$secret"; \ + fi + @if ! "$(KUBECTL)" -n orka-system get secret workspace-publisher-auth >/dev/null 2>&1; then \ + bearer="$$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n')"; \ + capability="$$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n')"; \ + "$(KUBECTL)" -n orka-system create secret generic workspace-publisher-auth --from-literal=controller-token="$$bearer" --from-literal=operation-capability-secret="$$capability"; \ + fi + @if ! "$(KUBECTL)" -n orka-system get secret provider-auth-proxy >/dev/null 2>&1; then \ token="$$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n')"; \ - "$(KUBECTL)" -n orka-system create secret generic harness-wrapper-auth --from-literal=token="$$token"; \ + "$(KUBECTL)" -n orka-system create secret generic provider-auth-proxy --from-literal=token="$$token"; \ fi - "$(KUSTOMIZE)" build config/default | \ - sed -E \ - -e 's|^([[:space:]]*- --ai-worker-image=).*$$|\1$(AI_WORKER_IMG)|' \ - -e 's|^([[:space:]]*- --general-worker-image=).*$$|\1$(GENERAL_WORKER_IMG)|' | \ - "$(KUBECTL)" apply -f - + @if ! "$(KUBECTL)" -n orka-system get secret scm-egress-proxy-auth >/dev/null 2>&1; then \ + token="$$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"; \ + "$(KUBECTL)" -n orka-system create secret generic scm-egress-proxy-auth --from-literal=token="$$token"; \ + fi + @set -eu; tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \ + cp -R config "$$tmp/config"; \ + "$(CURDIR)/scripts/render-worker-images.sh" "$$tmp/config/manager/manager.yaml" \ + "$(AI_WORKER_IMG)" "$(GENERAL_WORKER_IMG)"; \ + "$(CURDIR)/scripts/render-acp-runtime-images.sh" "$$tmp/config/acp-production" \ + "${ACP_CODEX_RUNTIME_IMG}" "${ACP_CLAUDE_RUNTIME_IMG}" "${ACP_COPILOT_RUNTIME_IMG}"; \ + cd "$$tmp/config/acp-production"; \ + "$(KUSTOMIZE)" edit set image \ + ghcr.io/orka-agents/orka=${IMG} \ + docker.io/sozercan/orka-workspace-publisher=${WORKSPACE_PUBLISHER_IMG}; \ + "$(CURDIR)/scripts/apply-acp-production.sh" "$$PWD" "$(KUSTOMIZE)" "$(KUBECTL)" + .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f - + "$(KUSTOMIZE)" build config/acp-production | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f - ##@ Dependencies diff --git a/NOTICE.md b/NOTICE.md index 44afa52c8..81f872cdb 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -25,7 +25,7 @@ upstream revision, and Orka modification notes. ## GitHub Copilot CLI -Orka embeds the GitHub Copilot CLI in the `agent-harness-wrapper` binary using `github.com/github/copilot-sdk/go/cmd/bundler`. The embedded CLI version is resolved at build time by the bundler based on the `github.com/github/copilot-sdk/go` version in `go.mod`. +The digest-pinned Copilot ACP runtime image redistributes the unmodified official GitHub Copilot CLI executable as one component of Orka's fenced RuntimePool service. The image includes this license at `/usr/share/licenses/github-copilot-cli/LICENSE.md`; the controller and Publisher images do not include Copilot CLI. GitHub Copilot CLI License @@ -65,7 +65,7 @@ GitHub Copilot CLI License ## GitHub Copilot SDK for Go -Orka uses `github.com/github/copilot-sdk/go` to integrate with GitHub Copilot CLI. +The source dependency graph includes `github.com/github/copilot-sdk/go` for compatibility and integration work. The supported built-in ACP runtime profiles are Codex, Claude, and Copilot. MIT License @@ -91,13 +91,13 @@ SOFTWARE. ## Anthropic Claude Code -Orka installs `@anthropic-ai/claude-code` and the corresponding Linux platform package in the agent harness image. +The digest-pinned Claude ACP runtime image installs `@anthropic-ai/claude-code` and the corresponding Linux platform package. The controller and Publisher images do not include Claude Code. © Anthropic PBC. All rights reserved. Use is subject to the Legal Agreements outlined here: https://code.claude.com/docs/en/legal-and-compliance. ## OpenAI Codex CLI -Orka installs `@openai/codex` and the corresponding Linux platform package in the agent harness image. The npm package declares the Apache License 2.0. +The digest-pinned Codex ACP runtime image installs `@openai/codex` and the corresponding Linux platform package. The controller and Publisher images do not include Codex. The npm package declares the Apache License 2.0. Apache License Version 2.0, January 2004 diff --git a/README.md b/README.md index 26a3d8127..76c7f76ec 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ --- -Orka turns your Kubernetes cluster into an AI-powered task execution platform. Spin up swarms of AI agents that write code, review PRs, research topics, or run containers — each as an isolated Kubernetes Job with full scheduling, retries, and observability. A coordinator agent dynamically decomposes complex tasks, spawns specialist agents to work in parallel, and synthesizes their results — no manual orchestration graphs required. +Orka turns your Kubernetes cluster into an AI-powered task execution platform. Native AI and container work run as Kubernetes Jobs; ACP coding agents run as fenced RuntimeSessions in controller-owned, scale-to-zero RuntimePools. A coordinator agent dynamically decomposes complex tasks, spawns specialist agents to work in parallel, and synthesizes their results — no manual orchestration graphs required. One `helm install`, one LLM secret, and you're chatting with an orchestrator that handles the rest. @@ -28,9 +28,9 @@ One `helm install`, one LLM secret, and you're chatting with an orchestrator tha **Centralized control** — One place to set model policies, rate limits, and allowed providers across every team. Swap models or providers without touching developer configs. -**Every agent action is auditable** — Tasks run as Kubernetes Jobs with full logs, Prometheus metrics, and result storage. Know exactly what every agent did, when, and at what cost. +**Every agent action is auditable** — Tasks have durable execution events, Prometheus metrics, structured results, and, for ACP agents, fenced attempt/session and delivery receipts. Know exactly what every agent did, when, and at what cost. -**Isolated execution** — Each agent runs in its own Pod with a hardened security context: non-root, read-only rootfs, all capabilities dropped, seccomp enforced. Agents can't escape their sandbox. +**Hardened execution** — Native workers use hardened per-Task Pods. ACP runtimes use digest-pinned shared Pods with private per-session directories and identities; a RuntimePool is a same-trust-domain boundary, not cross-tenant isolation. **Scale with your cluster** — Priority scheduling, retry policies, concurrency limits, and cron-based execution — all handled by the Kubernetes control plane you already operate. @@ -51,7 +51,7 @@ One `helm install`, one LLM secret, and you're chatting with an orchestrator tha ## Features - 🤖 **AI Agents** — Anthropic, OpenAI, or Azure OpenAI with tools, skills, and session persistence -- 🛠️ **Agent Runtimes** — Delegate repo-backed coding tasks to Codex CLI, Claude Code CLI, GitHub Copilot CLI, or OpenCode CLI +- 🛠️ **ACP Agent Runtimes** — Run Codex, Claude, and Copilot through digest-pinned RuntimePools; external `orka.harness.v2` registration and conformance are available while `runtimeRef` Task dispatch remains fail-closed - 🔁 **Autonomous Task Loops** — Coordinators can iterate on long-running goals until complete, canceled, or at an iteration limit - 🔀 **Multi-Agent Coordination** — Coordinators delegate to specialists with depth and concurrency controls - 💬 **Interactive Chat** — Agentic orchestrator with SSE streaming that creates and manages agents and tasks for you @@ -59,15 +59,24 @@ One `helm install`, one LLM secret, and you're chatting with an orchestrator tha - 🧠 **Durable Memory** — Namespace-scoped recall, transcript search, and reviewable memory proposals that can be applied - 🛡️ **Repository Security Scanning** — Scheduled and incremental repository scans with threat models, validated findings, patch generation, and remediation PRs - 🔎 **Repository Monitors** — Durable GitHub PR review queues with scheduled and webhook-triggered review runs -- 🧰 **Agent Sandbox Workspaces** — Experimental durable, reusable coding workspaces through `agent-sandbox` +- 🧰 **Deferred Workspace Providers** — Evaluate `agent-sandbox` or Substrate separately; neither is a current ACP execution path - 🖥️ **Web Dashboard** — Built-in React UI embedded in the controller binary — zero extra deployments -- 📦 **Declarative CRDs** — Task, Agent, AgentRuntime, Tool, Provider, Skill, RepositoryScan, RepositoryMonitor, and SubstrateActorPool custom resources for GitOps workflows +- 📦 **Declarative Control** — Workload, gateway, workspace, and Kubernetes-authoritative ACP control CRDs for GitOps workflows - ⏰ **Scheduled Tasks** — Cron-based recurring execution with concurrency policies - 🔌 **REST & OpenAI-Compatible API** — Full CRUD + `/openai/v1/chat/completions` endpoint for Continue, Cursor, and any OpenAI-compatible client - 🔐 **Kubernetes, OIDC & Transaction-Token Auth** — ServiceAccount tokens by default, with optional OIDC and scoped vendor-neutral transaction governance - 🔮 **Anthropic-Compatible API** — `/anthropic/v1/messages` endpoint for Claude Code and other Anthropic-native clients - 📊 **Observability** — Prometheus metrics, structured logging, health probes, and optional OpenTelemetry traces + GenAI OTLP metrics -- 🔒 **Hardened by Default** — Non-root containers, read-only rootfs, ServiceAccount token auth +- 🔒 **Hardened by Default** — Non-root native workers, fenced private ACP child identities, read-only filesystems, and authenticated broker boundaries + +The ACP hard cutover keeps control authority in Kubernetes: +`ControllerEpoch`, `PromptAttempt`, `RuntimeSessionControl`, `BranchClaim`, +`Publication`, and `ExternalEffect` status plus coordination Leases. SQLite is +limited to transcript/SessionTurn payloads, deferred outbox projections, and +artifact payloads (including result bodies). Provider traffic uses the central authenticated proxy; +prompt tools use prompt-scoped MCP; and source-read, target-read, target-write, +and forge credentials reach only the clean-room Publisher through the +credential broker. Artifact access is separately operation-scoped. ## Quick Start @@ -76,38 +85,29 @@ One `helm install`, one LLM secret, and you're chatting with an orchestrator tha ```bash helm install orka charts/orka \ --namespace orka-system \ - --create-namespace + --create-namespace \ + --set controller.image.repository=docker.io/sozercan/orka \ + --set controller.image.digest=sha256: \ + --set publisher.image.repository=docker.io/sozercan/orka-workspace-publisher \ + --set publisher.image.digest=sha256: \ + --set controller.acpRuntime.codexImage=docker.io/sozercan/orka-acp-codex@sha256: \ + --set controller.acpRuntime.claudeImage=docker.io/sozercan/orka-acp-claude@sha256: ``` -A fresh install creates all twelve cluster-scoped Orka CRDs. Use `--skip-crds` -only when one designated platform or release owner already manages compatible -Orka CRDs for the cluster. - -> [!IMPORTANT] -> Helm does not create or update files from `crds/` during `helm upgrade`. -> Apply the CRDs from the exact target chart before -> **every** upgrade, including an upgrade from the previous chart that installed -> zero CRDs. Helm retains CRDs -> on uninstall. See the [Helm CRD lifecycle guide](charts/orka/README.md). - -For the promoted raw installer, pre-create the harness-wrapper authentication -Secret before applying the manifest; the token is intentionally not committed: +For direct Kustomize deployments, use `config/acp-production`, not +`config/default`. The production overlay includes the cross-namespace Vekil +ingress policy that permits model traffic only through Orka's authenticated +provider proxy: ```bash -set -euo pipefail - -kubectl create namespace orka-system --dry-run=client -o yaml | kubectl apply -f - -if ! kubectl -n orka-system get secret harness-wrapper-auth >/dev/null 2>&1; then - openssl rand -hex 32 | \ - kubectl -n orka-system create secret generic harness-wrapper-auth \ - --from-file=token=/dev/stdin -fi - -kubectl apply -f deploy/orka.yaml +kubectl apply -k config/acp-production ``` -See [`config/harness-wrapper/README.md`](config/harness-wrapper/README.md) for -the canonical installer prerequisite. +Provision the required system Secrets and digest-pinned images before applying +the overlay; `make deploy` performs those checks and applies the equivalent +resource set. + +A fresh Helm install creates the chart CRDs unless `--skip-crds` is used. Helm does not update CRDs during `helm upgrade`, so apply the CRDs from the exact target chart before every controller upgrade. Designate one lifecycle owner for cluster-scoped CRDs and see the [Helm CRD lifecycle guide](charts/orka/README.md). ### Set Up a Provider @@ -129,12 +129,18 @@ spec: EOF ``` +That `Provider` Secret is used by native `type: ai` Tasks and the compatible +chat APIs. Built-in ACP Agents do **not** reference provider Secrets. Codex and +Claude RuntimeSessions reach Vekil only through the central authenticated +provider proxy. Source-read, target-read, target-write, and forge credentials +are brokered separately to the clean-room Workspace/Publisher. + ### Start Chatting Use the built-in dashboard, or connect any OpenAI-compatible client: ```bash -kubectl port-forward -n orka-system svc/orka-api 8080:8080 +kubectl port-forward -n orka-system svc/orka 8080:8080 # Open the web dashboard open http://localhost:8080 @@ -150,9 +156,9 @@ The built-in orchestrator creates agents, runs tasks, monitors progress, and ret | [Architecture](website/docs/concepts/architecture.md) | System design, components, and data flow | | [Configuration](website/docs/concepts/configuration.md) | CRD reference, Helm values, controller flags, metrics | | [Observability](website/docs/guides/observability.md) | OpenTelemetry traces, GenAI metrics, and task trace guidance | -| [Agent Runtimes](website/docs/concepts/agent-runtimes.md) | Built-in CLI runtimes and bring-your-own remote AgentRuntime backends | -| [CLI Harness Wrapper](website/docs/guides/cli-harness-wrapper.md) | Harness protocol wrapper for Codex, Claude, Copilot, and OpenCode CLI runtimes | -| [Agent Sandbox](website/docs/concepts/agent-sandbox.md) | Experimental upstream `agent-sandbox` workspace execution for agent runtimes | +| [Agent Runtimes](website/docs/concepts/agent-runtimes.md) | ACP v2 RuntimePools, workspace policy, delivery, and external registrations | +| [AgentRuntime Adapter Contract](website/docs/development/agent-runtime-adapter-contract.md) | Portable `orka.harness.v2` session and fencing contract | +| [Agent Sandbox](website/docs/concepts/agent-sandbox.md) | Deferred execution-workspace integration behind the ACP v2 lifecycle | | [Interactive Chat](website/docs/guides/chat.md) | Chat endpoint, tools, and SSE streaming | | [Multi-Agent Coordination](website/docs/guides/multi-agent-coordination.md) | Coordinator agents and task delegation | | [Autonomous Tasks](website/docs/guides/autonomous-tasks.md) | Long-running coordinator loops with persisted plan state | diff --git a/api/v1alpha1/agent_runtime_crd_types.go b/api/v1alpha1/agent_runtime_crd_types.go index 7d54c6ec9..1accaeac0 100644 --- a/api/v1alpha1/agent_runtime_crd_types.go +++ b/api/v1alpha1/agent_runtime_crd_types.go @@ -6,15 +6,19 @@ MIT License - see LICENSE file for details. package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // AgentRuntimeContractVersion identifies the Orka-facing runtime contract. -// +kubebuilder:validation:Enum=orka.harness.v1 +// +kubebuilder:validation:Enum=orka.harness.v2 type AgentRuntimeContractVersion string const ( - // AgentRuntimeContractHarnessV1 is the frozen harness v1 HTTP+SSE contract. - AgentRuntimeContractHarnessV1 AgentRuntimeContractVersion = "orka.harness.v1" + // AgentRuntimeContractHarnessV2 is the session-centric HTTP+NDJSON contract. + AgentRuntimeContractHarnessV2 AgentRuntimeContractVersion = "orka.harness.v2" ) // AgentRuntimeDeploymentMode selects how the runtime endpoint is provided. @@ -26,27 +30,14 @@ const ( AgentRuntimeDeploymentModeExternalEndpoint AgentRuntimeDeploymentMode = "external-endpoint" ) -// AgentRuntimeToolExecutionMode declares how custom runtimes interact with tools. -// +kubebuilder:validation:Enum=observed;brokered -type AgentRuntimeToolExecutionMode string - -const ( - // AgentRuntimeToolExecutionModeObserved means the runtime owns its internal tools and Orka observes lifecycle only. - AgentRuntimeToolExecutionModeObserved AgentRuntimeToolExecutionMode = "observed" - // AgentRuntimeToolExecutionModeBrokered means the runtime asks Orka to execute governed Tool CRDs. - AgentRuntimeToolExecutionModeBrokered AgentRuntimeToolExecutionMode = "brokered" -) - -// AgentRuntimeBrokeredToolClass declares which classes of Orka-brokered tools a runtime can request. +// AgentRuntimeBrokeredToolClass classifies Tool CRDs and remains shared by the +// Tool API. It is no longer an AgentRuntime capability field. // +kubebuilder:validation:Enum=read;write;coordination type AgentRuntimeBrokeredToolClass string const ( - // AgentRuntimeBrokeredToolClassRead covers read-only evidence and lookup tools. - AgentRuntimeBrokeredToolClassRead AgentRuntimeBrokeredToolClass = "read" - // AgentRuntimeBrokeredToolClassWrite covers consequential tools that may require approval and idempotency. - AgentRuntimeBrokeredToolClassWrite AgentRuntimeBrokeredToolClass = "write" - // AgentRuntimeBrokeredToolClassCoordination covers Orka coordination tools such as delegate_task/wait_for_tasks. + AgentRuntimeBrokeredToolClassRead AgentRuntimeBrokeredToolClass = "read" + AgentRuntimeBrokeredToolClassWrite AgentRuntimeBrokeredToolClass = "write" AgentRuntimeBrokeredToolClassCoordination AgentRuntimeBrokeredToolClass = "coordination" ) @@ -59,156 +50,280 @@ type AgentRuntimeReference struct { // AgentRuntimeDeploymentSpec configures where Orka reaches the harness runtime. type AgentRuntimeDeploymentSpec struct { - // Mode is the deployment mode. The first milestone supports external endpoints only. + // Mode is the deployment mode. External AgentRuntime registrations are not + // scaled or recycled by Orka. // +kubebuilder:validation:Required Mode AgentRuntimeDeploymentMode `json:"mode"` - // Endpoint is the base URL for a pre-deployed or external orka.harness.v1 service. - // It must not contain credentials; bearer auth is configured via clientAuth. + // Endpoint is the base URL for an external orka.harness.v2 service. It must + // not contain credentials, query parameters, or fragments. // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern=`^https?://[^\s@?#]+$` Endpoint string `json:"endpoint"` } -// AgentRuntimeBearerAuthReference identifies the Secret key holding a harness bearer token. -type AgentRuntimeBearerAuthReference struct { - // Name is the Secret name. +// AgentRuntimeSecretKeyReference identifies one Secret key used for v2 control traffic. +type AgentRuntimeSecretKeyReference struct { + // Name is the Secret name in the AgentRuntime namespace. // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` Name string `json:"name"` - // Key is the Secret data key containing the bearer token. + // Key is the Secret data key. // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 Key string `json:"key"` } -// AgentRuntimeClientAuth configures Orka's client authentication to the harness endpoint. +// AgentRuntimeClientAuth configures controller authentication and per-operation authorization. type AgentRuntimeClientAuth struct { - // BearerAuthRef points to the bearer token Secret used for mutating harness endpoints. - // The referenced Secret must opt in with label orka.ai/agent-runtime-auth=true, - // may set orka.ai/agent-runtime-name= to restrict use to one AgentRuntime, - // and must set annotation orka.ai/agent-runtime-endpoint= to bind the token to one endpoint. + // ControllerBearerTokenSecretRef supplies the controller bearer token used by + // authenticated v2 status and mutation endpoints. // +kubebuilder:validation:Required - BearerAuthRef AgentRuntimeBearerAuthReference `json:"bearerTokenSecretRef"` -} + ControllerBearerTokenSecretRef AgentRuntimeSecretKeyReference `json:"controllerBearerTokenSecretRef"` -// AgentRuntimeCapabilitiesSpec describes required capabilities for runtime readiness. -type AgentRuntimeCapabilitiesSpec struct { - // ToolExecutionModes lists tool execution modes the runtime must advertise. - // +listType=set - // +optional - ToolExecutionModes []AgentRuntimeToolExecutionMode `json:"toolExecutionModes,omitempty"` + // OperationCapabilitySecretRef supplies the HMAC secret used to bind every + // mutation to its exact fence, operation identity, request digest, and expiry. + // +kubebuilder:validation:Required + OperationCapabilitySecretRef AgentRuntimeSecretKeyReference `json:"operationCapabilitySecretRef"` +} - // BrokeredToolClasses lists brokered tool classes the runtime must advertise when brokered mode is required. - // +listType=set - // +optional - BrokeredToolClasses []AgentRuntimeBrokeredToolClass `json:"brokeredToolClasses,omitempty"` +// AgentRuntimeWorkspaceGovernanceMode describes whether Orka may rely on the +// runtime for strict workspace guarantees. +// +kubebuilder:validation:Enum=strict-governed;trusted-non-governed +type AgentRuntimeWorkspaceGovernanceMode string - // SupportsCancel requires the runtime to advertise cancellation support when true. - // +optional - SupportsCancel *bool `json:"supportsCancel,omitempty"` +const ( + // AgentRuntimeWorkspaceGovernanceStrict is eligible only for the exact + // workspace intent pinned in the immutable runtime profile. + AgentRuntimeWorkspaceGovernanceStrict AgentRuntimeWorkspaceGovernanceMode = "strict-governed" + // AgentRuntimeWorkspaceGovernanceTrusted marks an explicitly trusted runtime + // whose tools and workspace behavior are outside Orka governance. + AgentRuntimeWorkspaceGovernanceTrusted AgentRuntimeWorkspaceGovernanceMode = "trusted-non-governed" +) - // SupportsRuntimeSessions requires the runtime to advertise stable runtime sessions when true. - // +optional - SupportsRuntimeSessions *bool `json:"supportsRuntimeSessions,omitempty"` +// AgentRuntimeWorkspaceGovernanceCapabilities are static claims advertised by +// /v2/capabilities and exercised by the hostile conformance cycle. +// +kubebuilder:validation:XValidation:rule="self.mode != 'trusted-non-governed' || self.trusted",message="trusted-non-governed runtimes must be explicitly marked trusted" +// +kubebuilder:validation:XValidation:rule="self.mode != 'strict-governed' || !self.trusted",message="strict-governed runtimes must not use the trusted non-governed escape hatch" +// +kubebuilder:validation:XValidation:rule="self.mode != 'strict-governed' || (self.orkaOwnedWorkspaceDeltas && self.promptScopedBrokerAuthorization && self.noDirectSCMPublication && self.orkaOwnedCleanRoomPublication && self.exactInstanceFencing && self.duplicateSafeMutations && self.cancellationSettlement)",message="strict-governed runtimes must claim every strict workspace governance guarantee" +// +kubebuilder:validation:XValidation:rule="self.mode != 'trusted-non-governed' || (!self.orkaOwnedWorkspaceDeltas && !self.promptScopedBrokerAuthorization && !self.noDirectSCMPublication && !self.orkaOwnedCleanRoomPublication && !self.exactInstanceFencing && !self.duplicateSafeMutations && !self.cancellationSettlement)",message="trusted-non-governed runtimes must not claim strict workspace guarantees" +type AgentRuntimeWorkspaceGovernanceCapabilities struct { + // Mode selects strict Orka governance or an explicit trusted escape hatch. + // +kubebuilder:validation:Required + Mode AgentRuntimeWorkspaceGovernanceMode `json:"mode"` + + // Trusted must be true only for trusted-non-governed runtimes. Such runtimes + // are ineligible for Tasks requesting strict read or write guarantees. + Trusted bool `json:"trusted"` + + OrkaOwnedWorkspaceDeltas bool `json:"orkaOwnedWorkspaceDeltas"` + PromptScopedBrokerAuthorization bool `json:"promptScopedBrokerAuthorization"` + NoDirectSCMPublication bool `json:"noDirectSCMPublication"` + OrkaOwnedCleanRoomPublication bool `json:"orkaOwnedCleanRoomPublication"` + ExactInstanceFencing bool `json:"exactInstanceFencing"` + DuplicateSafeMutations bool `json:"duplicateSafeMutations"` + CancellationSettlement bool `json:"cancellationSettlement"` +} - // SupportsContinuation requires the runtime to advertise continuation after Orka-brokered tool results when true. - // +optional - SupportsContinuation *bool `json:"supportsContinuation,omitempty"` +// Strict reports whether every strict workspace governance guarantee is claimed. +func (c AgentRuntimeWorkspaceGovernanceCapabilities) Strict() bool { + return c.Mode == AgentRuntimeWorkspaceGovernanceStrict && !c.Trusted && + c.OrkaOwnedWorkspaceDeltas && c.PromptScopedBrokerAuthorization && + c.NoDirectSCMPublication && c.OrkaOwnedCleanRoomPublication && + c.ExactInstanceFencing && c.DuplicateSafeMutations && c.CancellationSettlement +} - // SupportsArtifacts requires the runtime to advertise artifact/result reference support when true. - // +optional - SupportsArtifacts *bool `json:"supportsArtifacts,omitempty"` +// AgentRuntimeProtocolLimits pins the exact bounded v2 limits expected from the runtime. +type AgentRuntimeProtocolLimits struct { + // +kubebuilder:validation:Minimum=1 + MaxResidentSessions int32 `json:"maxResidentSessions"` + // +kubebuilder:validation:Minimum=1 + MaxConcurrentPrompts int32 `json:"maxConcurrentPrompts"` + // +kubebuilder:validation:Minimum=1 + MaxRequestBytes int32 `json:"maxRequestBytes"` + // +kubebuilder:validation:Minimum=1 + MaxEventLineBytes int32 `json:"maxEventLineBytes"` + // +kubebuilder:validation:Minimum=1 + MaxTerminalResultBytes int32 `json:"maxTerminalResultBytes"` + // +kubebuilder:validation:Minimum=1 + MaxBufferedEvents int32 `json:"maxBufferedEvents"` + // +kubebuilder:validation:Minimum=1 + MaxUpdateEventsPerSecond int32 `json:"maxUpdateEventsPerSecond"` + // +kubebuilder:validation:Minimum=1 + MinPromptLeaseMillis int64 `json:"minPromptLeaseMillis"` + // +kubebuilder:validation:Minimum=1 + MaxPromptLeaseMillis int64 `json:"maxPromptLeaseMillis"` + // +kubebuilder:validation:Minimum=1 + MaxPendingPermissions int32 `json:"maxPendingPermissions"` + // +kubebuilder:validation:Minimum=1 + MaxWorkspaceDeltaBytes int64 `json:"maxWorkspaceDeltaBytes"` } -// AgentRuntimeRegistrySpec defines the desired state of a registered Orka harness runtime. -type AgentRuntimeRegistrySpec struct { - // ContractVersion is the Orka harness contract this runtime must implement. +// AgentRuntimeProfileSpec pins one immutable, single-adapter v2 runtime profile. +type AgentRuntimeProfileSpec struct { + // Digest is the canonical orka.harness.v2 runtime-profile digest. // +kubebuilder:validation:Required - // +kubebuilder:default=orka.harness.v1 - ContractVersion AgentRuntimeContractVersion `json:"contractVersion"` + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + Digest string `json:"digest"` - // Deployment identifies the runtime endpoint provider. + // DigestSchemaVersion identifies the canonical profile digest schema. // +kubebuilder:validation:Required - Deployment AgentRuntimeDeploymentSpec `json:"deployment"` + // +kubebuilder:validation:Enum=1 + DigestSchemaVersion int32 `json:"digestSchemaVersion"` - // ClientAuth configures controller-to-runtime authentication. + // ACPProfile is the reviewed ACP profile. // +kubebuilder:validation:Required - ClientAuth AgentRuntimeClientAuth `json:"clientAuth"` + // +kubebuilder:validation:Enum=acp.v1 + ACPProfile string `json:"acpProfile"` - // Capabilities declares readiness requirements Orka checks against the runtime. - // +optional - Capabilities *AgentRuntimeCapabilitiesSpec `json:"capabilities,omitempty"` -} + // AdapterName identifies the sole adapter contained by this external profile. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + AdapterName string `json:"adapterName"` -// AgentRuntimeObservedCapabilities records the sanitized capability data observed by Orka. -type AgentRuntimeObservedCapabilities struct { - // ProtocolVersion is the runtime's advertised Orka protocol version. - // +optional - ProtocolVersion string `json:"protocolVersion,omitempty"` + // AdapterDigest pins the adapter/CLI artifact set. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + AdapterDigest string `json:"adapterDigest"` - // Transport is the runtime transport, normally http+sse. - // +optional - Transport string `json:"transport,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + ProviderKind string `json:"providerKind"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + Model string `json:"model"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + AgentConfigurationDigest string `json:"agentConfigurationDigest"` + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ToolPolicyDigest string `json:"toolPolicyDigest"` + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ApprovalPolicyDigest string `json:"approvalPolicyDigest"` + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + MCPConfigurationDigest string `json:"mcpConfigurationDigest"` + + // WorkspaceIntent is the one immutable strict intent represented by this profile. + // +kubebuilder:validation:Required + WorkspaceIntent WorkspaceIntent `json:"workspaceIntent"` - // RuntimeName is the runtime name advertised by /v1/capabilities. - // +optional - RuntimeName string `json:"runtimeName,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + ProxyCredentialRole string `json:"proxyCredentialRole"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ProxyCredentialScope string `json:"proxyCredentialScope"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + ResourceClass string `json:"resourceClass"` +} - // RuntimeVersion is the runtime version advertised by /v1/capabilities. - // +optional - RuntimeVersion string `json:"runtimeVersion,omitempty"` +// AgentRuntimeCapabilitiesSpec pins the exact static and authenticated runtime identity. +type AgentRuntimeCapabilitiesSpec struct { + // RuntimeInstanceID is the immutable external supervisor instance expected from + // authenticated /v2/status and every conformance response. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + RuntimeInstanceID string `json:"runtimeInstanceID"` - // ProviderKind is the provider kind advertised by /v1/capabilities. - // +optional - ProviderKind string `json:"providerKind,omitempty"` + // Profile is the exact immutable profile accepted by session creation. + // +kubebuilder:validation:Required + Profile AgentRuntimeProfileSpec `json:"profile"` - // ToolExecutionModes are the tool modes advertised by /v1/capabilities. - // +listType=set - // +optional - ToolExecutionModes []AgentRuntimeToolExecutionMode `json:"toolExecutionModes,omitempty"` + // Limits must exactly match /v2/capabilities. + // +kubebuilder:validation:Required + Limits AgentRuntimeProtocolLimits `json:"limits"` - // BrokeredToolClasses are the brokered tool classes advertised by /v1/capabilities. - // +listType=set - // +optional - BrokeredToolClasses []AgentRuntimeBrokeredToolClass `json:"brokeredToolClasses,omitempty"` + // SupportsDrain must exactly match the static capability claim. + SupportsDrain bool `json:"supportsDrain"` - // SupportsCancel reports whether the runtime advertises cancellation support. + // SupportsPublicationFinalization must exactly match the static capability claim. // +optional - SupportsCancel bool `json:"supportsCancel,omitempty"` + SupportsPublicationFinalization bool `json:"supportsPublicationFinalization,omitempty"` - // SupportsRuntimeSessions reports whether the runtime advertises runtime-session support. - // +optional - SupportsRuntimeSessions bool `json:"supportsRuntimeSessions,omitempty"` + // WorkspaceGovernance must exactly match the static capability claim. + // +kubebuilder:validation:Required + WorkspaceGovernance AgentRuntimeWorkspaceGovernanceCapabilities `json:"workspaceGovernance"` +} - // SupportsContinuation reports whether the runtime advertises continuation support. - // +optional - SupportsContinuation bool `json:"supportsContinuation,omitempty"` +// SupportsStrictWorkspaceIntent returns true only for the exact intent pinned +// by a fully governed profile. Trusted/non-governed runtimes always return false. +func (c AgentRuntimeCapabilitiesSpec) SupportsStrictWorkspaceIntent(intent WorkspaceIntent) bool { + return c.WorkspaceGovernance.Strict() && c.Profile.WorkspaceIntent == intent && + (intent != WorkspaceIntentWrite || c.SupportsPublicationFinalization) +} - // SupportsArtifacts reports whether the runtime advertises artifact/result reference support. - // +optional - SupportsArtifacts bool `json:"supportsArtifacts,omitempty"` +// ValidateStrictWorkspaceIntent rejects trusted/non-governed runtimes and exact-profile intent mismatches. +func (c AgentRuntimeCapabilitiesSpec) ValidateStrictWorkspaceIntent(intent WorkspaceIntent) error { + if intent != WorkspaceIntentRead && intent != WorkspaceIntentWrite { + return fmt.Errorf("unsupported strict workspace intent %q", intent) + } + if c.WorkspaceGovernance.Mode == AgentRuntimeWorkspaceGovernanceTrusted { + return fmt.Errorf("trusted-non-governed AgentRuntime cannot satisfy strict %q workspace intent", intent) + } + if !c.WorkspaceGovernance.Strict() { + return fmt.Errorf("AgentRuntime does not provide all strict workspace governance guarantees") + } + if c.Profile.WorkspaceIntent != intent { + return fmt.Errorf("AgentRuntime profile is pinned to workspace intent %q, not %q", c.Profile.WorkspaceIntent, intent) + } + if intent == WorkspaceIntentWrite && !c.SupportsPublicationFinalization { + return fmt.Errorf("AgentRuntime does not support controller-owned RuntimeSession publication finalization required for write workspaces") + } + return nil +} - // SupportsSuspend reports whether the runtime advertises suspend support. - // +optional - SupportsSuspend bool `json:"supportsSuspend,omitempty"` +// AgentRuntimeRegistrySpec defines the desired state of a registered Orka harness runtime. +type AgentRuntimeRegistrySpec struct { + // ContractVersion is the Orka harness contract this runtime must implement. + // +kubebuilder:validation:Required + // +kubebuilder:default=orka.harness.v2 + ContractVersion AgentRuntimeContractVersion `json:"contractVersion"` - // SupportsWorkspaceSnapshot reports whether the runtime advertises workspace snapshots. - // +optional - SupportsWorkspaceSnapshot bool `json:"supportsWorkspaceSnapshot,omitempty"` + // Deployment identifies the runtime endpoint provider. + // +kubebuilder:validation:Required + Deployment AgentRuntimeDeploymentSpec `json:"deployment"` - // MaxConcurrentTurns is the advertised concurrency ceiling. - // +optional - MaxConcurrentTurns int `json:"maxConcurrentTurns,omitempty"` + // ClientAuth configures controller authentication and mutation authorization. + // +kubebuilder:validation:Required + ClientAuth AgentRuntimeClientAuth `json:"clientAuth"` - // MaxTurnSeconds is the advertised per-turn duration ceiling. - // +optional - MaxTurnSeconds int `json:"maxTurnSeconds,omitempty"` + // Capabilities pins the exact instance, profile, limits, and governance claims. + // +kubebuilder:validation:Required + Capabilities *AgentRuntimeCapabilitiesSpec `json:"capabilities"` +} - // MaxOutputBytes is the advertised maximum output payload size. - // +optional - MaxOutputBytes int64 `json:"maxOutputBytes,omitempty"` +// AgentRuntimeObservedCapabilities records sanitized v2 conformance data. +type AgentRuntimeObservedCapabilities struct { + ProtocolVersion string `json:"protocolVersion,omitempty"` + Transport string `json:"transport,omitempty"` + ACPVersion string `json:"acpVersion,omitempty"` + RuntimeInstanceID string `json:"runtimeInstanceID,omitempty"` + SupervisorBootID string `json:"supervisorBootID,omitempty"` + ControllerEpoch int64 `json:"controllerEpoch,omitempty"` + RuntimePoolUID string `json:"runtimePoolUID,omitempty"` + RuntimePoolGeneration int64 `json:"runtimePoolGeneration,omitempty"` + RuntimeProfileDigest string `json:"runtimeProfileDigest,omitempty"` + ProfileDigestSchemaVersion int32 `json:"profileDigestSchemaVersion,omitempty"` + AdapterName string `json:"adapterName,omitempty"` + AdapterDigest string `json:"adapterDigest,omitempty"` + ProviderKind string `json:"providerKind,omitempty"` + Model string `json:"model,omitempty"` + Limits AgentRuntimeProtocolLimits `json:"limits,omitempty"` + SupportsDrain bool `json:"supportsDrain,omitempty"` + SupportsPublicationFinalization bool `json:"supportsPublicationFinalization,omitempty"` + WorkspaceGovernance AgentRuntimeWorkspaceGovernanceCapabilities `json:"workspaceGovernance,omitempty"` + Lifecycle string `json:"lifecycle,omitempty"` } // AgentRuntimeStatus defines the observed state of an AgentRuntime. @@ -229,11 +344,19 @@ type AgentRuntimeStatus struct { // +optional LastValidated *metav1.Time `json:"lastValidated,omitempty"` - // ObservedAuthRefResourceVersion is the resourceVersion of the bearer auth Secret - // used for the last readiness probe. It is non-secret metadata used to decide - // when token rotation requires a fresh authenticated conformance turn. + // ObservedControllerAuthRefResourceVersion is the bearer Secret version used + // by the last successful or failed authenticated conformance probe. // +optional - ObservedAuthRefResourceVersion string `json:"observedAuthRefResourceVersion,omitempty"` + ObservedControllerAuthRefResourceVersion string `json:"observedControllerAuthRefResourceVersion,omitempty"` + + // ObservedOperationCapabilityRefResourceVersion is the HMAC Secret version used + // by the last mutation conformance probe. + // +optional + ObservedOperationCapabilityRefResourceVersion string `json:"observedOperationCapabilityRefResourceVersion,omitempty"` + + // ObservedAuthRefResourceVersion keeps old Go-only routing code compilable. + // Deprecated: use the two v2 auth resource-version fields. + ObservedAuthRefResourceVersion string `json:"-"` // Message provides sanitized readiness context. // +optional @@ -252,10 +375,10 @@ type AgentRuntimeStatus struct { // +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready` // +kubebuilder:printcolumn:name="Contract",type=string,JSONPath=`.spec.contractVersion` // +kubebuilder:printcolumn:name="Mode",type=string,JSONPath=`.spec.deployment.mode` -// +kubebuilder:printcolumn:name="Runtime",type=string,JSONPath=`.status.observedCapabilities.runtimeName` +// +kubebuilder:printcolumn:name="Instance",type=string,JSONPath=`.status.observedCapabilities.runtimeInstanceID` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// AgentRuntime is the Schema for registered Orka harness runtimes. +// AgentRuntime is the Schema for registered external Orka harness runtimes. type AgentRuntime struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/api/v1alpha1/agent_runtime_types_test.go b/api/v1alpha1/agent_runtime_types_test.go index 7e6b0ea18..7c6e3d16b 100644 --- a/api/v1alpha1/agent_runtime_types_test.go +++ b/api/v1alpha1/agent_runtime_types_test.go @@ -7,6 +7,11 @@ MIT License - see LICENSE file for details. package v1alpha1 import ( + "encoding/json" + "os" + "path/filepath" + goruntime "runtime" + "strings" "testing" corev1 "k8s.io/api/core/v1" @@ -16,6 +21,10 @@ const ( testExecutionRuntimeClassGVisor = "gvisor" testExecutionRuntimeClassKata = "kata-qemu" testExecutionNodeLabelKey = "sandbox-runtime" + testAgentRuntimeControllerKey = "controller-token" + testAgentRuntimeCapabilityKey = "capability-secret" + testAgentRuntimeInstanceID = "runtime-instance-1" + testAgentRuntimeAuthSecretName = "runtime-auth" ) func TestTaskTypeAgentConstant(t *testing.T) { @@ -63,6 +72,41 @@ func TestAgentRuntimeTypeConstants(t *testing.T) { } } +func readTaskTypesSource(t *testing.T) []byte { + t.Helper() + _, testFile, _, ok := goruntime.Caller(0) + if !ok { + t.Fatal("resolve test source path") + } + paths := []string{filepath.Join(filepath.Dir(testFile), "task_types.go"), "task_types.go"} + var lastErr error + for _, path := range paths { + source, err := os.ReadFile(path) + if err == nil { + return source + } + lastErr = err + } + t.Fatalf("read task_types.go: %v", lastErr) + return nil +} + +func TestAgentRuntimeTypeKubebuilderEnumIncludesSupportedBuiltIns(t *testing.T) { + source := readTaskTypesSource(t) + const marker = "// +kubebuilder:validation:Enum=claude;codex;copilot" + if !strings.Contains(string(source), marker) { + t.Fatalf("AgentRuntimeType marker does not include all supported built-ins: want %q", marker) + } +} + +func TestAgentPromptImmutabilityMarkerHandlesOmittedPrompt(t *testing.T) { + source := readTaskTypesSource(t) + const marker = "has(self.prompt) == has(oldSelf.prompt)" + if !strings.Contains(string(source), marker) { + t.Fatalf("agent prompt immutability marker is not presence-aware: want %q", marker) + } +} + func TestAgentRuntimeSpecFields(t *testing.T) { maxTurns := int32(10) allowBash := true @@ -71,10 +115,6 @@ func TestAgentRuntimeSpecFields(t *testing.T) { AllowedTools: []string{"read", "write"}, DisallowedTools: []string{"delete"}, AllowBash: &allowBash, - Workspace: &WorkspaceConfig{ - GitRepo: "https://github.com/example/repo", - Branch: "main", - }, } if *spec.MaxTurns != 10 { @@ -92,12 +132,6 @@ func TestAgentRuntimeSpecFields(t *testing.T) { if *spec.AllowBash != true { t.Errorf("AllowBash = %v, want true", *spec.AllowBash) } - if spec.Workspace == nil { - t.Fatal("Workspace should not be nil") - } - if spec.Workspace.GitRepo != "https://github.com/example/repo" { - t.Errorf("Workspace.GitRepo = %q, want %q", spec.Workspace.GitRepo, "https://github.com/example/repo") - } } func TestAgentRuntimeSpecDefaults(t *testing.T) { @@ -115,19 +149,19 @@ func TestAgentRuntimeSpecDefaults(t *testing.T) { if spec.AllowBash != nil { t.Errorf("AllowBash should be nil by default, got %v", spec.AllowBash) } - if spec.Workspace != nil { - t.Errorf("Workspace should be nil by default, got %v", spec.Workspace) - } } func TestWorkspaceConfigFields(t *testing.T) { - secretRef := &corev1.LocalObjectReference{Name: "git-secret"} + readRef := &WorkspaceCredentialReference{Name: "git-read"} + publicationRef := &WorkspaceCredentialReference{Name: "git-publish"} ws := WorkspaceConfig{ - GitRepo: "https://github.com/example/repo", - Branch: "develop", - Ref: "abc123", - GitSecretRef: secretRef, - SubPath: "src/app", + GitRepo: "https://github.com/example/repo", + Branch: "develop", + Ref: "abc123", + ReadCredentialRef: readRef, + PublicationGitRepo: "https://github.com/example/repo-fork", + PublicationCredentialRef: publicationRef, + SubPath: "src/app", } if ws.GitRepo != "https://github.com/example/repo" { @@ -139,8 +173,14 @@ func TestWorkspaceConfigFields(t *testing.T) { if ws.Ref != "abc123" { t.Errorf("Ref = %q, want %q", ws.Ref, "abc123") } - if ws.GitSecretRef == nil || ws.GitSecretRef.Name != "git-secret" { - t.Errorf("GitSecretRef.Name = %v, want %q", ws.GitSecretRef, "git-secret") + if ws.ReadCredentialRef == nil || ws.ReadCredentialRef.Name != "git-read" { + t.Errorf("ReadCredentialRef.Name = %v, want %q", ws.ReadCredentialRef, "git-read") + } + if ws.PublicationCredentialRef == nil || ws.PublicationCredentialRef.Name != "git-publish" { + t.Errorf("PublicationCredentialRef.Name = %v, want %q", ws.PublicationCredentialRef, "git-publish") + } + if ws.PublicationGitRepo != "https://github.com/example/repo-fork" { + t.Errorf("PublicationGitRepo = %q, want %q", ws.PublicationGitRepo, "https://github.com/example/repo-fork") } if ws.SubPath != "src/app" { t.Errorf("SubPath = %q, want %q", ws.SubPath, "src/app") @@ -159,8 +199,11 @@ func TestWorkspaceConfigDefaults(t *testing.T) { if ws.Ref != "" { t.Errorf("Ref should be empty by default, got %q", ws.Ref) } - if ws.GitSecretRef != nil { - t.Errorf("GitSecretRef should be nil by default, got %v", ws.GitSecretRef) + if ws.ReadCredentialRef != nil { + t.Errorf("ReadCredentialRef should be nil by default, got %v", ws.ReadCredentialRef) + } + if ws.PublicationCredentialRef != nil { + t.Errorf("PublicationCredentialRef should be nil by default, got %v", ws.PublicationCredentialRef) } if ws.SubPath != "" { t.Errorf("SubPath should be empty by default, got %q", ws.SubPath) @@ -231,10 +274,10 @@ func TestTaskSpecAgentRuntimeField(t *testing.T) { AgentRuntime: &AgentRuntimeSpec{ MaxTurns: &maxTurns, AllowedTools: []string{"bash", "read"}, - Workspace: &WorkspaceConfig{ - GitRepo: "https://github.com/example/repo", - Branch: "main", - }, + }, + Workspace: &WorkspaceConfig{ + GitRepo: "https://github.com/example/repo", + Branch: "main", }, } @@ -247,11 +290,11 @@ func TestTaskSpecAgentRuntimeField(t *testing.T) { if *task.AgentRuntime.MaxTurns != 15 { t.Errorf("AgentRuntime.MaxTurns = %d, want 15", *task.AgentRuntime.MaxTurns) } - if task.AgentRuntime.Workspace == nil { - t.Fatal("AgentRuntime.Workspace should not be nil") + if task.Workspace == nil { + t.Fatal("Task.Workspace should not be nil") } - if task.AgentRuntime.Workspace.Branch != "main" { - t.Errorf("Workspace.Branch = %q, want %q", task.AgentRuntime.Workspace.Branch, "main") + if task.Workspace.Branch != "main" { + t.Errorf("Workspace.Branch = %q, want %q", task.Workspace.Branch, "main") } } @@ -378,40 +421,145 @@ func TestAgentRuntimeReferenceOnAgentCLI(t *testing.T) { } func TestAgentRuntimeCRDSpecFields(t *testing.T) { - supportsCancel := true - supportsContinuation := true + strict := AgentRuntimeWorkspaceGovernanceCapabilities{ + Mode: AgentRuntimeWorkspaceGovernanceStrict, + OrkaOwnedWorkspaceDeltas: true, + PromptScopedBrokerAuthorization: true, + NoDirectSCMPublication: true, + OrkaOwnedCleanRoomPublication: true, + ExactInstanceFencing: true, + DuplicateSafeMutations: true, + CancellationSettlement: true, + } runtime := AgentRuntime{ Spec: AgentRuntimeRegistrySpec{ - ContractVersion: AgentRuntimeContractHarnessV1, + ContractVersion: AgentRuntimeContractHarnessV2, Deployment: AgentRuntimeDeploymentSpec{ Mode: AgentRuntimeDeploymentModeExternalEndpoint, - Endpoint: "http://fibey-agentkit.default.svc.cluster.local:8080", + Endpoint: "https://runtime.example.com", + }, + ClientAuth: AgentRuntimeClientAuth{ + ControllerBearerTokenSecretRef: AgentRuntimeSecretKeyReference{Name: testAgentRuntimeAuthSecretName, Key: testAgentRuntimeControllerKey}, + OperationCapabilitySecretRef: AgentRuntimeSecretKeyReference{Name: testAgentRuntimeAuthSecretName, Key: testAgentRuntimeCapabilityKey}, }, - ClientAuth: AgentRuntimeClientAuth{BearerAuthRef: AgentRuntimeBearerAuthReference{ - Name: "fibey-agentkit-harness-token", - Key: "token", - }}, Capabilities: &AgentRuntimeCapabilitiesSpec{ - ToolExecutionModes: []AgentRuntimeToolExecutionMode{AgentRuntimeToolExecutionModeObserved, AgentRuntimeToolExecutionModeBrokered}, - BrokeredToolClasses: []AgentRuntimeBrokeredToolClass{AgentRuntimeBrokeredToolClassRead}, - SupportsCancel: &supportsCancel, - SupportsContinuation: &supportsContinuation, + RuntimeInstanceID: testAgentRuntimeInstanceID, + Profile: AgentRuntimeProfileSpec{ + Digest: "sha256:" + strings.Repeat("a", 64), DigestSchemaVersion: 1, + ACPProfile: "acp.v1", AdapterName: "codex", AdapterDigest: "sha256:" + strings.Repeat("b", 64), + ProviderKind: "codex", Model: "gpt-test", + AgentConfigurationDigest: "sha256:" + strings.Repeat("c", 64), + ToolPolicyDigest: "sha256:" + strings.Repeat("d", 64), ApprovalPolicyDigest: "sha256:" + strings.Repeat("e", 64), + MCPConfigurationDigest: "sha256:" + strings.Repeat("f", 64), WorkspaceIntent: WorkspaceIntentRead, + ProxyCredentialRole: "provider-proxy", ProxyCredentialScope: "session-and-prompt", ResourceClass: "standard", + }, + Limits: AgentRuntimeProtocolLimits{ + MaxResidentSessions: 10, MaxConcurrentPrompts: 4, MaxRequestBytes: 1 << 20, + MaxEventLineBytes: 1 << 20, MaxTerminalResultBytes: 1 << 20, MaxBufferedEvents: 256, + MaxUpdateEventsPerSecond: 100, MinPromptLeaseMillis: 5000, MaxPromptLeaseMillis: 120000, + MaxPendingPermissions: 32, MaxWorkspaceDeltaBytes: 512 << 20, + }, + SupportsDrain: true, + WorkspaceGovernance: strict, }, }, } - if runtime.Spec.ContractVersion != AgentRuntimeContractHarnessV1 { + if runtime.Spec.ContractVersion != AgentRuntimeContractHarnessV2 { t.Fatalf("ContractVersion = %q", runtime.Spec.ContractVersion) } - if runtime.Spec.Deployment.Mode != AgentRuntimeDeploymentModeExternalEndpoint { - t.Fatalf("Deployment.Mode = %q", runtime.Spec.Deployment.Mode) + if runtime.Spec.ClientAuth.ControllerBearerTokenSecretRef.Key != testAgentRuntimeControllerKey || + runtime.Spec.ClientAuth.OperationCapabilitySecretRef.Key != testAgentRuntimeCapabilityKey { + t.Fatalf("ClientAuth = %#v", runtime.Spec.ClientAuth) + } + if !runtime.Spec.Capabilities.SupportsStrictWorkspaceIntent(WorkspaceIntentRead) { + t.Fatal("strict read profile was not eligible for strict read intent") + } + if runtime.Spec.Capabilities.SupportsStrictWorkspaceIntent(WorkspaceIntentWrite) { + t.Fatal("exact read profile was incorrectly eligible for strict write intent") + } +} + +func TestAgentRuntimeWriteIntentRequiresPublicationFinalization(t *testing.T) { + capabilities := AgentRuntimeCapabilitiesSpec{ + Profile: AgentRuntimeProfileSpec{WorkspaceIntent: WorkspaceIntentWrite}, + WorkspaceGovernance: AgentRuntimeWorkspaceGovernanceCapabilities{ + Mode: AgentRuntimeWorkspaceGovernanceStrict, OrkaOwnedWorkspaceDeltas: true, PromptScopedBrokerAuthorization: true, + NoDirectSCMPublication: true, OrkaOwnedCleanRoomPublication: true, ExactInstanceFencing: true, + DuplicateSafeMutations: true, CancellationSettlement: true, + }, + } + if capabilities.SupportsStrictWorkspaceIntent(WorkspaceIntentWrite) { + t.Fatal("write runtime without publication finalization was strict-write eligible") } - if runtime.Spec.ClientAuth.BearerAuthRef.Name != "fibey-agentkit-harness-token" { - t.Fatalf("BearerAuthRef.Name = %q", runtime.Spec.ClientAuth.BearerAuthRef.Name) + if err := capabilities.ValidateStrictWorkspaceIntent(WorkspaceIntentWrite); err == nil || !strings.Contains(err.Error(), "publication finalization") { + t.Fatalf("ValidateStrictWorkspaceIntent(write) = %v, want publication-finalization rejection", err) } - if len(runtime.Spec.Capabilities.BrokeredToolClasses) != 1 || runtime.Spec.Capabilities.BrokeredToolClasses[0] != AgentRuntimeBrokeredToolClassRead { - t.Fatalf("BrokeredToolClasses = %#v", runtime.Spec.Capabilities.BrokeredToolClasses) + capabilities.SupportsPublicationFinalization = true + if !capabilities.SupportsStrictWorkspaceIntent(WorkspaceIntentWrite) { + t.Fatal("write runtime with publication finalization was not strict-write eligible") } - if runtime.Spec.Capabilities.SupportsContinuation == nil || !*runtime.Spec.Capabilities.SupportsContinuation { - t.Fatalf("SupportsContinuation = %#v, want true", runtime.Spec.Capabilities.SupportsContinuation) + if err := capabilities.ValidateStrictWorkspaceIntent(WorkspaceIntentWrite); err != nil { + t.Fatalf("ValidateStrictWorkspaceIntent(write) = %v", err) + } +} + +func TestAgentRuntimeTrustedNonGovernedIsNeverStrictEligible(t *testing.T) { + capabilities := AgentRuntimeCapabilitiesSpec{ + Profile: AgentRuntimeProfileSpec{WorkspaceIntent: WorkspaceIntentRead}, + WorkspaceGovernance: AgentRuntimeWorkspaceGovernanceCapabilities{ + Mode: AgentRuntimeWorkspaceGovernanceTrusted, + Trusted: true, + }, + } + if capabilities.SupportsStrictWorkspaceIntent(WorkspaceIntentRead) || capabilities.SupportsStrictWorkspaceIntent(WorkspaceIntentWrite) { + t.Fatal("trusted non-governed runtime was eligible for strict workspace intent") + } + for _, intent := range []WorkspaceIntent{WorkspaceIntentRead, WorkspaceIntentWrite} { + if err := capabilities.ValidateStrictWorkspaceIntent(intent); err == nil || !strings.Contains(err.Error(), "trusted-non-governed") { + t.Fatalf("ValidateStrictWorkspaceIntent(%q) = %v, want explicit trusted rejection", intent, err) + } + } +} + +func TestAgentRuntimeV1CapabilityFieldsAreAbsentFromSerializedCRDSurface(t *testing.T) { + digest := func(char string) string { return "sha256:" + strings.Repeat(char, 64) } + profile := AgentRuntimeProfileSpec{ + Digest: digest("a"), DigestSchemaVersion: 1, ACPProfile: "acp.v1", AdapterName: "codex", AdapterDigest: digest("b"), + ProviderKind: "codex", Model: "gpt-test", AgentConfigurationDigest: digest("c"), ToolPolicyDigest: digest("d"), + ApprovalPolicyDigest: digest("e"), MCPConfigurationDigest: digest("f"), WorkspaceIntent: WorkspaceIntentRead, + ProxyCredentialRole: "provider-proxy", ProxyCredentialScope: "model:gpt-test", ResourceClass: "standard", + } + claims := AgentRuntimeWorkspaceGovernanceCapabilities{ + Mode: AgentRuntimeWorkspaceGovernanceStrict, OrkaOwnedWorkspaceDeltas: true, PromptScopedBrokerAuthorization: true, + NoDirectSCMPublication: true, OrkaOwnedCleanRoomPublication: true, ExactInstanceFencing: true, + DuplicateSafeMutations: true, CancellationSettlement: true, + } + limits := AgentRuntimeProtocolLimits{ + MaxResidentSessions: 10, MaxConcurrentPrompts: 4, MaxRequestBytes: 1 << 20, MaxEventLineBytes: 1 << 20, + MaxTerminalResultBytes: 1 << 20, MaxBufferedEvents: 256, MaxUpdateEventsPerSecond: 100, + MinPromptLeaseMillis: 5000, MaxPromptLeaseMillis: 120000, MaxPendingPermissions: 32, MaxWorkspaceDeltaBytes: 100 << 20, + } + spec := AgentRuntimeRegistrySpec{ + ContractVersion: AgentRuntimeContractHarnessV2, + ClientAuth: AgentRuntimeClientAuth{ + ControllerBearerTokenSecretRef: AgentRuntimeSecretKeyReference{Name: testAgentRuntimeAuthSecretName, Key: testAgentRuntimeControllerKey}, + OperationCapabilitySecretRef: AgentRuntimeSecretKeyReference{Name: testAgentRuntimeAuthSecretName, Key: testAgentRuntimeCapabilityKey}, + }, + Capabilities: &AgentRuntimeCapabilitiesSpec{RuntimeInstanceID: testAgentRuntimeInstanceID, Profile: profile, Limits: limits, SupportsDrain: true, WorkspaceGovernance: claims}, + } + encoded, err := json.Marshal(spec) + if err != nil { + t.Fatal(err) + } + serialized := string(encoded) + for _, forbidden := range []string{"orka.harness.v1", "bearerTokenSecretRef", "toolExecutionModes", "brokeredToolClasses", "supportsContinuation", "supportsRuntimeSessions", "supportsArtifacts"} { + if strings.Contains(serialized, forbidden) { + t.Fatalf("serialized AgentRuntime still contains v1-only field %q: %s", forbidden, serialized) + } + } + for _, required := range []string{"orka.harness.v2", "controllerBearerTokenSecretRef", "operationCapabilitySecretRef", "runtimeInstanceID", "workspaceGovernance"} { + if !strings.Contains(serialized, required) { + t.Fatalf("serialized AgentRuntime is missing v2 field %q: %s", required, serialized) + } } } diff --git a/api/v1alpha1/agent_types.go b/api/v1alpha1/agent_types.go index 15a5697ca..fca1af152 100644 --- a/api/v1alpha1/agent_types.go +++ b/api/v1alpha1/agent_types.go @@ -133,7 +133,6 @@ type ModelConfig struct { // Temperature controls randomness in generation // +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=2 - // +kubebuilder:default=0.7 // +optional Temperature *float64 `json:"temperature,omitempty"` @@ -148,6 +147,7 @@ type ModelConfig struct { } // PromptSource defines where to get a prompt from +// +kubebuilder:validation:XValidation:rule="!(has(self.inline) && self.inline.size() > 0 && has(self.configMapRef))",message="system prompt must use only one of inline or configMapRef" type PromptSource struct { // Inline is the inline prompt text // +optional diff --git a/api/v1alpha1/branch_claim_types.go b/api/v1alpha1/branch_claim_types.go new file mode 100644 index 000000000..0a2d282b2 --- /dev/null +++ b/api/v1alpha1/branch_claim_types.go @@ -0,0 +1,101 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// BranchClaimOwnerKind identifies the durable owner of an Orka-managed branch. +// +kubebuilder:validation:Enum=Task;Session +type BranchClaimOwnerKind string + +// BranchClaimAvailability gates further branch mutation. +// +kubebuilder:validation:Enum=Available;ReconciliationBlocked +type BranchClaimAvailability string + +// BranchClaimSpec is the immutable repository/ref ownership identity. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="branch claim spec is immutable" +type BranchClaimSpec struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ID string `json:"id"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + RepositoryID string `json:"repositoryId"` + + // +kubebuilder:validation:Pattern=`^refs/heads/.+$` + // +kubebuilder:validation:MaxLength=1024 + Ref string `json:"ref"` + + OwnerKind BranchClaimOwnerKind `json:"ownerKind"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + OwnerUID string `json:"ownerUid"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` +} + +// BranchClaimStatus is the exact generation, baseline, and availability CAS. +// +kubebuilder:validation:XValidation:rule="!has(self.availability) || self.availability != 'Available' || ((!has(self.blockedReason) || size(self.blockedReason) == 0) && (!has(self.relatedPublicationId) || size(self.relatedPublicationId) == 0))",message="available branch claims must clear block metadata" +// +kubebuilder:validation:XValidation:rule="!has(self.availability) || self.availability != 'ReconciliationBlocked' || (has(self.blockedReason) && size(self.blockedReason) > 0)",message="reconciliation-blocked branch claims require a reason" +type BranchClaimStatus struct { + // +optional + // +kubebuilder:validation:Minimum=1 + Generation int64 `json:"generation,omitempty"` + + // LastVerified is the independently observed exact target ref. + // +optional + LastVerified *ControlRemoteRefState `json:"lastVerified,omitempty"` + + // +optional + Availability BranchClaimAvailability `json:"availability,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=16384 + BlockedReason string `json:"blockedReason,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=1024 + RelatedPublicationID string `json:"relatedPublicationId,omitempty"` + + ControlRecordMutationStatus `json:",inline"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster,shortName=bclaim +// +kubebuilder:printcolumn:name="Repository",type=string,JSONPath=`.spec.repositoryId` +// +kubebuilder:printcolumn:name="Ref",type=string,JSONPath=`.spec.ref` +// +kubebuilder:printcolumn:name="Owner",type=string,JSONPath=`.spec.ownerKind` +// +kubebuilder:printcolumn:name="Generation",type=integer,JSONPath=`.status.generation` +// +kubebuilder:printcolumn:name="Availability",type=string,JSONPath=`.status.availability` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// BranchClaim is the cluster-wide Kubernetes-authoritative ownership and exact +// baseline record for one canonical repository branch. +type BranchClaim struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BranchClaimSpec `json:"spec"` + Status BranchClaimStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// BranchClaimList contains a list of BranchClaim. +type BranchClaimList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BranchClaim `json:"items"` +} + +func init() { + SchemeBuilder.Register(&BranchClaim{}, &BranchClaimList{}) +} diff --git a/api/v1alpha1/control_record_types.go b/api/v1alpha1/control_record_types.go new file mode 100644 index 000000000..d9cf61ed9 --- /dev/null +++ b/api/v1alpha1/control_record_types.go @@ -0,0 +1,117 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +const ( + // ControlRecordArtifactRetentionFinalizer prevents deletion while a control + // record can still retain or reconcile a content-addressed artifact. + ControlRecordArtifactRetentionFinalizer = "core.orka.ai/artifact-retention" + // ControlRecordLeaseProtectionFinalizer prevents deletion of a control record + // while a Kubernetes Lease still fences mutations for that record. + ControlRecordLeaseProtectionFinalizer = "core.orka.ai/lease-protection" + + // ControlRecordIDHashLabel stores a DNS-safe digest of the immutable logical + // record ID so namespaced records can be found without trusting mutable data. + ControlRecordIDHashLabel = "core.orka.ai/control-record-id-hash" + // ControlRecordTaskUIDLabel associates a controller-owned record with the + // immutable UID of the Task that caused it to be created. + ControlRecordTaskUIDLabel = "core.orka.ai/task-uid" +) + +// ControlRecordMutationStatus is the common fenced mutation metadata embedded +// in Kubernetes-authoritative ACP control records. Kubernetes resourceVersion +// provides the storage CAS; Version is the monotonic domain version expected by +// DurableControlStore callers. +type ControlRecordMutationStatus struct { + // ControllerEpochName identifies the controller epoch domain checked before + // the mutation. + // +optional + // +kubebuilder:validation:MaxLength=253 + ControllerEpochName string `json:"controllerEpochName,omitempty"` + + // ControllerEpoch is the exact epoch that performed the last mutation. + // +optional + // +kubebuilder:validation:Minimum=1 + ControllerEpoch int64 `json:"controllerEpoch,omitempty"` + + // ControllerEpochLeaseResourceVersion is the resourceVersion of the + // authoritative controller-epoch Lease observed by the mutation. + // +optional + // +kubebuilder:validation:MaxLength=64 + ControllerEpochLeaseResourceVersion string `json:"controllerEpochLeaseResourceVersion,omitempty"` + + // LastOperationID is the last idempotent mutation identity applied. + // +optional + // +kubebuilder:validation:MaxLength=1024 + LastOperationID string `json:"lastOperationId,omitempty"` + + // LastOperationDigest binds LastOperationID to exact canonical input. + // +optional + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + LastOperationDigest string `json:"lastOperationDigest,omitempty"` + + // Version is the monotonic domain CAS version. It advances once for each + // successfully persisted logical mutation. + // +optional + // +kubebuilder:validation:Minimum=1 + Version int64 `json:"version,omitempty"` + + // CreatedAt is the normalized logical creation time. + // +optional + CreatedAt *metav1.Time `json:"createdAt,omitempty"` + + // UpdatedAt is the normalized logical mutation time. + // +optional + UpdatedAt *metav1.Time `json:"updatedAt,omitempty"` +} + +// ControlRemoteRefState is an exact remote-ref observation. Absent and SHA are +// mutually exclusive. The all-zero value is reserved for an explicitly unknown +// observation in PublicationOutcomeUnknown receipts. +// +kubebuilder:validation:XValidation:rule="!(self.absent && has(self.sha) && size(self.sha) > 0)",message="absent and sha are mutually exclusive" +type ControlRemoteRefState struct { + Absent bool `json:"absent"` + + // +optional + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + SHA string `json:"sha,omitempty"` +} + +// ControlVerifiedBranchBaseline is an independently verified branch baseline. +type ControlVerifiedBranchBaseline struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + RepositoryID string `json:"repositoryId"` + + // +kubebuilder:validation:Pattern=`^refs/heads/.+$` + // +kubebuilder:validation:MaxLength=1024 + Ref string `json:"ref"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + SHA string `json:"sha"` +} + +// ControlRecordOwner identifies an immutable namespaced owner when the +// Kubernetes object name is not part of the DurableControlStore interface. +type ControlRecordOwner struct { + // +kubebuilder:validation:Enum=Task;Session;RuntimePool;PromptAttempt + Kind string `json:"kind"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + UID string `json:"uid"` +} + +// RBAC for the Kubernetes-authoritative ACP control-record store. These +// markers intentionally live with the API surface so generation does not +// require wiring the store into cmd/main.go. +// +kubebuilder:rbac:groups=core.orka.ai,resources=promptattempts;runtimesessioncontrols;branchclaims;publications;externaleffects;controllerepochs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core.orka.ai,resources=promptattempts/status;runtimesessioncontrols/status;branchclaims/status;publications/status;externaleffects/status;controllerepochs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=core.orka.ai,resources=promptattempts/finalizers;runtimesessioncontrols/finalizers;branchclaims/finalizers;publications/finalizers;externaleffects/finalizers;controllerepochs/finalizers,verbs=update +// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete diff --git a/api/v1alpha1/controller_epoch_types.go b/api/v1alpha1/controller_epoch_types.go new file mode 100644 index 000000000..5c968748f --- /dev/null +++ b/api/v1alpha1/controller_epoch_types.go @@ -0,0 +1,83 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ControllerEpochSpec is the immutable epoch-domain identity. The associated +// coordination.k8s.io Lease is the CAS authority for holder and epoch changes. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="controller epoch spec is immutable" +type ControllerEpochSpec struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Name string `json:"name"` +} + +// ControllerEpochStatus mirrors the authoritative Lease state for inspection +// and recovery. LeaseResourceVersion identifies the exact Lease revision. +type ControllerEpochStatus struct { + // +optional + // +kubebuilder:validation:Minimum=1 + Epoch int64 `json:"epoch,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=1024 + HolderID string `json:"holderId,omitempty"` + + // +optional + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest,omitempty"` + + // +optional + // +kubebuilder:validation:Minimum=1 + Version int64 `json:"version,omitempty"` + + // +optional + AcquiredAt *metav1.Time `json:"acquiredAt,omitempty"` + + // +optional + UpdatedAt *metav1.Time `json:"updatedAt,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=253 + LeaseName string `json:"leaseName,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=64 + LeaseResourceVersion string `json:"leaseResourceVersion,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=controllerepochs,scope=Namespaced,shortName=cepoch +// +kubebuilder:printcolumn:name="Epoch",type=integer,JSONPath=`.status.epoch` +// +kubebuilder:printcolumn:name="Holder",type=string,JSONPath=`.status.holderId` +// +kubebuilder:printcolumn:name="Version",type=integer,JSONPath=`.status.version` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// ControllerEpoch is the human-visible Kubernetes control record paired with +// an authoritative namespaced Lease. +type ControllerEpoch struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ControllerEpochSpec `json:"spec"` + Status ControllerEpochStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ControllerEpochList contains a list of ControllerEpoch. +type ControllerEpochList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ControllerEpoch `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ControllerEpoch{}, &ControllerEpochList{}) +} diff --git a/api/v1alpha1/execution_types.go b/api/v1alpha1/execution_types.go index 0a1baba1b..9f2064dda 100644 --- a/api/v1alpha1/execution_types.go +++ b/api/v1alpha1/execution_types.go @@ -26,10 +26,10 @@ type ExecutionSpec struct { // +optional Affinity *corev1.Affinity `json:"affinity,omitempty"` - // Workspace requests an upstream agent-sandbox execution workspace for agent Tasks. - // When enabled, the Task controller validates the request and propagates the - // resolved sandbox settings to the agent worker Job. The worker wrapper then - // claims the sandbox workspace and runs the configured agent runtime inside it. + // Workspace requests an execution workspace for worker-backed Task types. + // ACP core agent Tasks reject this field because their ephemeral workspace is + // owned by RuntimeSession lifecycle and clean-room publication. Actor-backed + // RuntimeSession support is a future integration behind the v2 lifecycle seam. // +optional Workspace *ExecutionWorkspaceSpec `json:"workspace,omitempty"` } diff --git a/api/v1alpha1/external_effect_types.go b/api/v1alpha1/external_effect_types.go new file mode 100644 index 000000000..88812c619 --- /dev/null +++ b/api/v1alpha1/external_effect_types.go @@ -0,0 +1,189 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ExternalEffectControlState is the durable state of one idempotent operation +// performed outside the controller's SQLite transaction boundary. +// +kubebuilder:validation:Enum=Pending;InFlight;Succeeded;Failed;OutcomeUnknown +type ExternalEffectControlState string + +// ExternalEffectSpec is the immutable canonical identity and request binding. +// The identity namespace intentionally duplicates metadata.namespace so a +// serialized record remains self-describing and can be checked fail-closed. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="external effect spec is immutable" +type ExternalEffectSpec struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ID string `json:"id"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + Kind string `json:"kind"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + IdentityNamespace string `json:"identityNamespace"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + AggregateID string `json:"aggregateId"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + OperationID string `json:"operationId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` +} + +// ExternalEffectStatus contains the mutable state, response, lease, and epoch +// fence for one canonical external effect. +// +kubebuilder:validation:XValidation:rule="!has(self.state) || self.state != 'InFlight' || (has(self.leaseOwner) && size(self.leaseOwner) > 0 && has(self.leaseExpiresAt))",message="in-flight external effects require a lease owner and expiry" +// +kubebuilder:validation:XValidation:rule="!has(self.state) || self.state == 'InFlight' || ((!has(self.leaseOwner) || size(self.leaseOwner) == 0) && !has(self.leaseExpiresAt))",message="non-in-flight external effects must clear lease fields" +type ExternalEffectStatus struct { + // +optional + State ExternalEffectControlState `json:"state,omitempty"` + + // +optional + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ResponseDigest string `json:"responseDigest,omitempty"` + + // Response stores a bounded JSON response for idempotent replay. Large + // response bodies should remain in the artifact store and be referenced by + // a compact receipt instead. + // +optional + Response *apiextensionsv1.JSON `json:"response,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=1024 + LeaseOwner string `json:"leaseOwner,omitempty"` + + // +optional + LeaseExpiresAt *metav1.Time `json:"leaseExpiresAt,omitempty"` + + // +optional + // +kubebuilder:validation:Minimum=0 + Attempts int64 `json:"attempts,omitempty"` + + ControlRecordMutationStatus `json:",inline"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=false +// +k8s:deepcopy-gen=false +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=eeffect +// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` +// +kubebuilder:printcolumn:name="Kind",type=string,JSONPath=`.spec.kind` +// +kubebuilder:printcolumn:name="Attempts",type=integer,JSONPath=`.status.attempts` +// +kubebuilder:printcolumn:name="Version",type=integer,JSONPath=`.status.version` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// ExternalEffect is the Kubernetes-authoritative canonical idempotency record +// for an operation outside SQLite. +type ExternalEffect struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ExternalEffectSpec `json:"spec"` + Status ExternalEffectStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=false +// +k8s:deepcopy-gen=false + +// ExternalEffectList contains a list of ExternalEffect. +type ExternalEffectList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ExternalEffect `json:"items"` +} + +// DeepCopyInto is hand-written so this new root type is usable before the +// repository owner regenerates zz_generated.deepcopy.go. +func (in *ExternalEffect) DeepCopyInto(out *ExternalEffect) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Status.Response != nil { + out.Status.Response = &apiextensionsv1.JSON{} + if in.Status.Response.Raw != nil { + out.Status.Response.Raw = append([]byte(nil), in.Status.Response.Raw...) + } + } + if in.Status.LeaseExpiresAt != nil { + out.Status.LeaseExpiresAt = in.Status.LeaseExpiresAt.DeepCopy() + } + deepCopyControlRecordMutationStatus(&in.Status.ControlRecordMutationStatus, &out.Status.ControlRecordMutationStatus) +} + +// DeepCopy creates an independent copy. +func (in *ExternalEffect) DeepCopy() *ExternalEffect { + if in == nil { + return nil + } + out := new(ExternalEffect) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject implements runtime.Object. +func (in *ExternalEffect) DeepCopyObject() runtime.Object { + if copy := in.DeepCopy(); copy != nil { + return copy + } + return nil +} + +// DeepCopyInto copies a list and all of its items. +func (in *ExternalEffectList) DeepCopyInto(out *ExternalEffectList) { + *out = *in + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + out.Items = make([]ExternalEffect, len(in.Items)) + for i := range in.Items { + in.Items[i].DeepCopyInto(&out.Items[i]) + } + } +} + +// DeepCopy creates an independent list copy. +func (in *ExternalEffectList) DeepCopy() *ExternalEffectList { + if in == nil { + return nil + } + out := new(ExternalEffectList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject implements runtime.Object. +func (in *ExternalEffectList) DeepCopyObject() runtime.Object { + if copy := in.DeepCopy(); copy != nil { + return copy + } + return nil +} + +func deepCopyControlRecordMutationStatus(in, out *ControlRecordMutationStatus) { + if in.CreatedAt != nil { + out.CreatedAt = in.CreatedAt.DeepCopy() + } + if in.UpdatedAt != nil { + out.UpdatedAt = in.UpdatedAt.DeepCopy() + } +} + +func init() { + SchemeBuilder.Register(&ExternalEffect{}, &ExternalEffectList{}) +} diff --git a/api/v1alpha1/external_effect_types_test.go b/api/v1alpha1/external_effect_types_test.go new file mode 100644 index 000000000..1333b99da --- /dev/null +++ b/api/v1alpha1/external_effect_types_test.go @@ -0,0 +1,55 @@ +package v1alpha1 + +import ( + "testing" + "time" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestExternalEffectDeepCopyIsIndependentAndRegistered(t *testing.T) { + expires := metav1.NewTime(time.Date(2026, time.July, 25, 6, 0, 0, 0, time.UTC)) + created := expires.DeepCopy() + updated := expires.DeepCopy() + original := &ExternalEffect{ + ObjectMeta: metav1.ObjectMeta{Name: "effect", Namespace: "tenant-a", Labels: map[string]string{"a": "b"}}, + Spec: ExternalEffectSpec{ID: "effect-1", Kind: "PullRequest", IdentityNamespace: "tenant-a", AggregateID: "publication-1", OperationID: "op-1", RequestDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + Status: ExternalEffectStatus{ + State: "InFlight", + Response: &apiextensionsv1.JSON{Raw: []byte(`{"ok":true}`)}, + LeaseExpiresAt: &expires, + ControlRecordMutationStatus: ControlRecordMutationStatus{ + CreatedAt: created, + UpdatedAt: updated, + }, + }, + } + copy := original.DeepCopy() + copy.Labels["a"] = "changed" + copy.Status.Response.Raw[2] = 'X' + copy.Status.LeaseExpiresAt.Time = copy.Status.LeaseExpiresAt.Add(time.Hour) + copy.Status.CreatedAt.Time = copy.Status.CreatedAt.Add(time.Hour) + + if original.Labels["a"] != "b" { + t.Fatalf("labels were aliased: %#v", original.Labels) + } + if string(original.Status.Response.Raw) != `{"ok":true}` { + t.Fatalf("response bytes were aliased: %s", original.Status.Response.Raw) + } + if !original.Status.LeaseExpiresAt.Equal(&expires) { + t.Fatalf("lease expiry was aliased: %s", original.Status.LeaseExpiresAt) + } + if !original.Status.CreatedAt.Equal(created) { + t.Fatalf("mutation timestamp was aliased: %s", original.Status.CreatedAt) + } + + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("AddToScheme: %v", err) + } + if _, err := scheme.New(GroupVersion.WithKind("ExternalEffect")); err != nil { + t.Fatalf("ExternalEffect is not registered: %v", err) + } +} diff --git a/api/v1alpha1/prompt_attempt_types.go b/api/v1alpha1/prompt_attempt_types.go new file mode 100644 index 000000000..fba10b8be --- /dev/null +++ b/api/v1alpha1/prompt_attempt_types.go @@ -0,0 +1,146 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// PromptAttemptExecutionState is the durable prompt execution state. +// +kubebuilder:validation:Enum=Queued;Reserved;SessionStarting;Planned;Submitting;SubmittedUnknown;Accepted;Running;Settling;Succeeded;Failed;Cancelled;OutcomeUnknown +type PromptAttemptExecutionState string + +// PromptAttemptDeliveryState is the durable delivery state for one prompt. +// +kubebuilder:validation:Enum=NotRequested;Validating;Preparing;Prepared;Publishing;Verifying;VerifiedExact;DeliveredSuperseded;ReadValidated;NoChange;CancelledBeforePublish;ReadOnlyWorkspaceModified;DeliveryConflict;CredentialBlocked;PublicationOutcomeUnknown +type PromptAttemptDeliveryState string + +// PromptCredentialBinding freezes one role-specific Secret identity without +// storing credential material. +type PromptCredentialBinding struct { + // +kubebuilder:validation:Enum=SourceRead;TargetRead;TargetWrite;Forge + Role string `json:"role"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Namespace string `json:"namespace"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + SecretName string `json:"secretName"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + SecretKey string `json:"secretKey"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + SecretUID string `json:"secretUid"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + ResourceVersion string `json:"resourceVersion"` +} + +// PromptAttemptSpec is the immutable identity and request binding for one +// Task prompt attempt. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="prompt attempt spec is immutable" +type PromptAttemptSpec struct { + // ID is the canonical DurableControlStore prompt-attempt ID. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ID string `json:"id"` + + // TaskUID is the immutable Kubernetes UID of the Task. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + TaskUID string `json:"taskUid"` + + // Attempt is the one-based Task attempt number. + // +kubebuilder:validation:Minimum=1 + Attempt int64 `json:"attempt"` + + // PromptID is the immutable prompt identity within the attempt. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + PromptID string `json:"promptId"` + + // RequestDigest binds the prompt identity to exact canonical input. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` + + // CredentialBindings is the immutable, role-separated Secret identity set. + // +optional + // +listType=map + // +listMapKey=role + // +kubebuilder:validation:MaxItems=4 + CredentialBindings []PromptCredentialBinding `json:"credentialBindings,omitempty"` +} + +// PromptAttemptStatus holds the exact execution and delivery state machines. +// +kubebuilder:validation:XValidation:rule="!has(self.executionState) || self.executionState != 'OutcomeUnknown' || (has(self.outcomeMarker) && size(self.outcomeMarker) > 0)",message="OutcomeUnknown requires an explicit outcome marker" +type PromptAttemptStatus struct { + // SessionUID is immutable after first binding. + // +optional + // +kubebuilder:validation:MaxLength=1024 + SessionUID string `json:"sessionUid,omitempty"` + + // SessionLeaseGeneration is immutable after first binding. + // +optional + // +kubebuilder:validation:Minimum=1 + SessionLeaseGeneration int64 `json:"sessionLeaseGeneration,omitempty"` + + // RuntimeInstanceID is immutable after first binding. + // +optional + // +kubebuilder:validation:MaxLength=1024 + RuntimeInstanceID string `json:"runtimeInstanceId,omitempty"` + + // +optional + ExecutionState PromptAttemptExecutionState `json:"executionState,omitempty"` + + // +optional + DeliveryState PromptAttemptDeliveryState `json:"deliveryState,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=16384 + TerminalReason string `json:"terminalReason,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=16384 + OutcomeMarker string `json:"outcomeMarker,omitempty"` + + ControlRecordMutationStatus `json:",inline"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=pattempt +// +kubebuilder:printcolumn:name="Execution",type=string,JSONPath=`.status.executionState` +// +kubebuilder:printcolumn:name="Delivery",type=string,JSONPath=`.status.deliveryState` +// +kubebuilder:printcolumn:name="Attempt",type=integer,JSONPath=`.spec.attempt` +// +kubebuilder:printcolumn:name="Version",type=integer,JSONPath=`.status.version` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// PromptAttempt is the Kubernetes-authoritative prompt execution and delivery +// control record. +type PromptAttempt struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PromptAttemptSpec `json:"spec"` + Status PromptAttemptStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PromptAttemptList contains a list of PromptAttempt. +type PromptAttemptList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PromptAttempt `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PromptAttempt{}, &PromptAttemptList{}) +} diff --git a/api/v1alpha1/publication_types.go b/api/v1alpha1/publication_types.go new file mode 100644 index 000000000..1256dbc26 --- /dev/null +++ b/api/v1alpha1/publication_types.go @@ -0,0 +1,313 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// PublicationControlState is the clean-room publication state machine. +// +kubebuilder:validation:Enum=Preparing;Prepared;Publishing;Verifying;VerifiedExact;DeliveredSuperseded;CancelledBeforePublish;DeliveryConflict;CredentialBlocked;PreparationFailed;PublicationOutcomeUnknown +type PublicationControlState string + +// PublicationSpec is the immutable clean-room publication identity and input. +// Mutable receipts and forge intent live only in status. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="publication spec is immutable" +type PublicationSpec struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ID string `json:"id"` + + // +kubebuilder:validation:Minimum=1 + Generation int64 `json:"generation"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + TaskUID string `json:"taskUid"` + + // +kubebuilder:validation:Minimum=1 + Attempt int64 `json:"attempt"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + PromptID string `json:"promptId"` + + // +optional + // +kubebuilder:validation:MaxLength=1024 + SessionUID string `json:"sessionUid,omitempty"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + BranchClaimID string `json:"branchClaimId"` + + // +kubebuilder:validation:Minimum=1 + BranchClaimGeneration int64 `json:"branchClaimGeneration"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + SourceRepositoryID string `json:"sourceRepositoryId"` + + // SourceRef is the exact immutable source ref or revision selector. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + SourceRef string `json:"sourceRef"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + SourceBaselineSHA string `json:"sourceBaselineSha"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + TargetRepositoryID string `json:"targetRepositoryId"` + + // +kubebuilder:validation:Pattern=`^refs/heads/.+$` + // +kubebuilder:validation:MaxLength=1024 + TargetRef string `json:"targetRef"` + + Baseline ControlRemoteRefState `json:"baseline"` + + // ArtifactID identifies the durable content-addressed change artifact. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ArtifactID string `json:"artifactId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ArtifactDigest string `json:"artifactDigest"` + + // +kubebuilder:validation:Minimum=1 + ArtifactSizeBytes int64 `json:"artifactSizeBytes"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + ArtifactMediaType string `json:"artifactMediaType"` + + // PublicationCredentialRef identifies an operation-scoped Secret reference; + // it never contains credential material. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + PublicationCredentialRef string `json:"publicationCredentialRef"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + CommitIdentity string `json:"commitIdentity"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=16384 + CommitMessage string `json:"commitMessage"` + + CommitTimestamp metav1.Time `json:"commitTimestamp"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` +} + +// PublicationPullRequestIntent is the exact forge tuple persisted before the +// first forge API call. +type PublicationPullRequestIntent struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + BaseRepositoryID string `json:"baseRepositoryId"` + + // +kubebuilder:validation:Pattern=`^refs/heads/.+$` + // +kubebuilder:validation:MaxLength=1024 + BaseRef string `json:"baseRef"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + HeadRepositoryID string `json:"headRepositoryId"` + + // +kubebuilder:validation:Pattern=`^refs/heads/.+$` + // +kubebuilder:validation:MaxLength=1024 + HeadRef string `json:"headRef"` + + // +kubebuilder:validation:Minimum=1 + PublicationGeneration int64 `json:"publicationGeneration"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + ExpectedHeadSHA string `json:"expectedHeadSha"` +} + +// PreparedPublicationControlReceipt records deterministic commit preparation. +type PreparedPublicationControlReceipt struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + OperationID string `json:"operationId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + TreeSHA string `json:"treeSha"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + CommitSHA string `json:"commitSha"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ManifestDigest string `json:"manifestDigest"` + + // RelativeRoot is the canonical repository-relative workspace root applied + // to every path in the immutable delta artifact. + // +optional + // +kubebuilder:validation:MaxLength=1024 + RelativeRoot string `json:"relativeRoot,omitempty"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + BundleArtifactID string `json:"bundleArtifactId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + BundleDigest string `json:"bundleDigest"` + + // +kubebuilder:validation:Minimum=1 + BundleSizeBytes int64 `json:"bundleSizeBytes"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + BundleMediaType string `json:"bundleMediaType"` + + // +kubebuilder:validation:Pattern=`^refs/orka/publications/[a-f0-9]{64}$` + BundleRef string `json:"bundleRef"` + + PreparedAt metav1.Time `json:"preparedAt"` +} + +// PublishOperationControlReceipt records the exact server-enforced ref CAS. +type PublishOperationControlReceipt struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + OperationID string `json:"operationId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + TargetRepositoryID string `json:"targetRepositoryId"` + + // +kubebuilder:validation:Pattern=`^refs/heads/.+$` + // +kubebuilder:validation:MaxLength=1024 + TargetRef string `json:"targetRef"` + + RemoteBefore ControlRemoteRefState `json:"remoteBefore"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + ExpectedCommitSHA string `json:"expectedCommitSha"` + + AcknowledgementUnknown bool `json:"acknowledgementUnknown"` + + PublishedAt metav1.Time `json:"publishedAt"` +} + +// PublicationVerificationControlReceipt is an independent remote observation. +type PublicationVerificationControlReceipt struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + OperationID string `json:"operationId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` + + Outcome PublicationControlState `json:"outcome"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + ExpectedCommitSHA string `json:"expectedCommitSha"` + + ObservedRemote ControlRemoteRefState `json:"observedRemote"` + + // +optional + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + DescendantProofDigest string `json:"descendantProofDigest,omitempty"` + + VerifiedAt metav1.Time `json:"verifiedAt"` +} + +// PullRequestOperationControlReceipt snapshots exact forge reconciliation. +type PullRequestOperationControlReceipt struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + OperationID string `json:"operationId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + IntentKey string `json:"intentKey"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ForgeID string `json:"forgeId"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + URL string `json:"url"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + State string `json:"state"` + + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + HeadSHA string `json:"headSha"` + + ReconciledAt metav1.Time `json:"reconciledAt"` +} + +// PublicationStatus contains mutable state, exact receipts, and epoch fencing. +// +kubebuilder:validation:XValidation:rule="!has(self.state) || !(self.state in ['DeliveryConflict', 'CredentialBlocked', 'PreparationFailed', 'PublicationOutcomeUnknown']) || (has(self.terminalReason) && size(self.terminalReason) > 0)",message="failure and unknown publication states require a terminal reason" +type PublicationStatus struct { + // +optional + State PublicationControlState `json:"state,omitempty"` + + // +optional + PRIntent *PublicationPullRequestIntent `json:"prIntent,omitempty"` + + // +optional + PreparedReceipt *PreparedPublicationControlReceipt `json:"preparedReceipt,omitempty"` + + // +optional + PublishReceipt *PublishOperationControlReceipt `json:"publishReceipt,omitempty"` + + // +optional + VerificationReceipt *PublicationVerificationControlReceipt `json:"verificationReceipt,omitempty"` + + // +optional + PullRequestReceipt *PullRequestOperationControlReceipt `json:"pullRequestReceipt,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=16384 + TerminalReason string `json:"terminalReason,omitempty"` + + ControlRecordMutationStatus `json:",inline"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=pubctl +// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` +// +kubebuilder:printcolumn:name="Generation",type=integer,JSONPath=`.spec.generation` +// +kubebuilder:printcolumn:name="Target",type=string,JSONPath=`.spec.targetRef` +// +kubebuilder:printcolumn:name="Version",type=integer,JSONPath=`.status.version` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// Publication is the Kubernetes-authoritative clean-room publication record. +type Publication struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PublicationSpec `json:"spec"` + Status PublicationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PublicationList contains a list of Publication. +type PublicationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Publication `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Publication{}, &PublicationList{}) +} diff --git a/api/v1alpha1/runtime_pool_types.go b/api/v1alpha1/runtime_pool_types.go new file mode 100644 index 000000000..309406610 --- /dev/null +++ b/api/v1alpha1/runtime_pool_types.go @@ -0,0 +1,537 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +const ( + // DefaultRuntimePoolDesiredReplicas keeps an idle pool scaled to zero until + // durable demand is present. + DefaultRuntimePoolDesiredReplicas int32 = 0 + // DefaultRuntimePoolMaxResidentSessions is the first-release per-pool + // resident-session limit. + DefaultRuntimePoolMaxResidentSessions int32 = 10 + // DefaultRuntimePoolMaxRunningPrompts is the first-release per-pool prompt + // concurrency limit. + DefaultRuntimePoolMaxRunningPrompts int32 = 4 + // DefaultRuntimePoolColdStartTimeoutSeconds bounds a 0 -> 1 pool startup. + DefaultRuntimePoolColdStartTimeoutSeconds int32 = 120 + // MaxRuntimePoolCapacityReservations bounds the durable reservation list in + // RuntimePool status. RuntimePool capacity is itself limited to 1000. + MaxRuntimePoolCapacityReservations = 1000 +) + +// RuntimePoolProtocolVersion is the controller-to-supervisor protocol profile. +// +kubebuilder:validation:Enum=orka.harness.v2 +type RuntimePoolProtocolVersion string + +const ( + // RuntimePoolProtocolHarnessV2 is the ACP session-centric harness contract. + RuntimePoolProtocolHarnessV2 RuntimePoolProtocolVersion = "orka.harness.v2" +) + +// RuntimePoolLifecycle is the controller-observed lifecycle of a logical pool. +// Only Serving may admit new RuntimeSessions. +// +kubebuilder:validation:Enum=Stopped;Starting;Serving;Draining;Quiescent;Stopping;Degraded;Ambiguous +type RuntimePoolLifecycle string + +const ( + RuntimePoolLifecycleStopped RuntimePoolLifecycle = "Stopped" + RuntimePoolLifecycleStarting RuntimePoolLifecycle = "Starting" + RuntimePoolLifecycleServing RuntimePoolLifecycle = "Serving" + RuntimePoolLifecycleDraining RuntimePoolLifecycle = "Draining" + RuntimePoolLifecycleQuiescent RuntimePoolLifecycle = "Quiescent" + RuntimePoolLifecycleStopping RuntimePoolLifecycle = "Stopping" + RuntimePoolLifecycleDegraded RuntimePoolLifecycle = "Degraded" + RuntimePoolLifecycleAmbiguous RuntimePoolLifecycle = "Ambiguous" +) + +// RuntimePoolAdmissionState is the authoritative admission gate for new +// RuntimeSessions. Existing session control traffic may continue while draining. +// +kubebuilder:validation:Enum=Closed;Accepting;Draining;Ambiguous +type RuntimePoolAdmissionState string + +const ( + RuntimePoolAdmissionClosed RuntimePoolAdmissionState = "Closed" + RuntimePoolAdmissionAccepting RuntimePoolAdmissionState = "Accepting" + RuntimePoolAdmissionDraining RuntimePoolAdmissionState = "Draining" + RuntimePoolAdmissionAmbiguous RuntimePoolAdmissionState = "Ambiguous" +) + +const ( + // RuntimePoolConditionAdmissionReady reports whether new RuntimeSessions may + // be admitted without violating lifecycle, fencing, or capacity rules. + RuntimePoolConditionAdmissionReady = "AdmissionReady" + // RuntimePoolConditionPodSecurityReady reports whether the runtime Pod passes + // the selected namespace's Pod Security admission requirements. + RuntimePoolConditionPodSecurityReady = "PodSecurityReady" + // RuntimePoolConditionQuotaReady reports whether quota permits the pool's + // controller-owned resources. + RuntimePoolConditionQuotaReady = "QuotaReady" + // RuntimePoolConditionSchedulingReady reports whether the selected runtime + // Pod can be scheduled. + RuntimePoolConditionSchedulingReady = "SchedulingReady" + // RuntimePoolConditionRolloutReady reports whether the immutable runtime + // profile has been installed without version skew. + RuntimePoolConditionRolloutReady = "RolloutReady" +) + +const ( + RuntimePoolReasonAdmissionClosed = "AdmissionClosed" + RuntimePoolReasonAtCapacity = "AtCapacity" + RuntimePoolReasonPodSecurityRejected = "PodSecurityRejected" + RuntimePoolReasonQuotaRejected = "QuotaRejected" + RuntimePoolReasonSchedulingFailed = "SchedulingFailed" + RuntimePoolReasonRolloutFailed = "RolloutFailed" + RuntimePoolReasonRuntimeAmbiguous = "RuntimeAmbiguous" +) + +// RuntimePoolTrustDomain identifies the logical same-trust-domain boundary +// served by one pool. It is not a tenant-isolation claim. +type RuntimePoolTrustDomain struct { + // Namespace is the Task namespace represented by this trust domain. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + Namespace string `json:"namespace"` + + // Identity is the controller-defined, canonical trust-domain identity. It + // must remain stable across physical runtime namespace or Pod replacement. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Identity string `json:"identity"` +} + +// RuntimePoolProfileSpec pins the immutable runtime behavior selected for a +// pool. The digest covers the adapter and CLI builds, ACP profile, provider and +// agent configuration, tool and approval policy, MCP configuration, workspace +// intent, proxy credential scope, and resource class. +type RuntimePoolProfileSpec struct { + // ProtocolVersion is the controller-to-supervisor protocol profile. + // +kubebuilder:default=orka.harness.v2 + // +optional + ProtocolVersion RuntimePoolProtocolVersion `json:"protocolVersion,omitempty"` + + // Digest is the canonical immutable runtime-profile digest. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + Digest string `json:"digest"` + + // DigestSchemaVersion identifies the canonicalization schema used to compute Digest. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + // +kubebuilder:validation:Pattern=`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` + DigestSchemaVersion string `json:"digestSchemaVersion"` + + // ACPProfile is the reviewed ACP wire/profile identifier. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=acp.v1 + ACPProfile string `json:"acpProfile"` + + // AdapterDigests pins every adapter and provider CLI artifact used by the pool. + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=32 + AdapterDigests map[string]string `json:"adapterDigests"` + + // ProviderKind selects the one provider adapter present in the immutable image. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=codex;claude;copilot + ProviderKind string `json:"providerKind"` + + // Model is the exact reviewed model identifier. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + Model string `json:"model"` + + // AgentConfigurationDigest freezes non-secret Agent/runtime configuration. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + AgentConfigurationDigest string `json:"agentConfigurationDigest"` + + // ToolPolicyDigest freezes the effective tool allow/deny policy. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ToolPolicyDigest string `json:"toolPolicyDigest"` + + // ApprovalPolicyDigest freezes the effective approval policy. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ApprovalPolicyDigest string `json:"approvalPolicyDigest"` + + // MCPConfigurationDigest freezes prompt-scoped broker/MCP configuration. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + MCPConfigurationDigest string `json:"mcpConfigurationDigest"` + + // WorkspaceIntent is part of the immutable runtime profile. + // +kubebuilder:validation:Required + WorkspaceIntent WorkspaceIntent `json:"workspaceIntent"` + + // ProxyCredentialRole identifies the provider-proxy client role, never a secret value. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + ProxyCredentialRole string `json:"proxyCredentialRole"` + + // ProxyCredentialScope is the bounded model/session capability scope. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + ProxyCredentialScope string `json:"proxyCredentialScope"` + + // ResourceClass is the controller-supported pool resource class included in Digest. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$` + ResourceClass string `json:"resourceClass"` +} + +// RuntimePoolRuntimeSpec selects the immutable supervisor image and profile. +type RuntimePoolRuntimeSpec struct { + // Image is a digest-pinned OCI image. Mutable tags are intentionally rejected. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=2048 + // +kubebuilder:validation:Pattern=`^[^\s@]+@sha256:[a-f0-9]{64}$` + Image string `json:"image"` + + // Profile is the immutable runtime profile enforced for every active instance. + // +kubebuilder:validation:Required + Profile RuntimePoolProfileSpec `json:"profile"` +} + +// RuntimePoolCapacitySpec defines hard logical pool concurrency limits. +// +kubebuilder:validation:XValidation:rule="self.maxRunningPrompts <= self.maxResidentSessions",message="maxRunningPrompts cannot exceed maxResidentSessions" +type RuntimePoolCapacitySpec struct { + // MaxResidentSessions is the maximum number of resident RuntimeSessions. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=1000 + // +kubebuilder:default=10 + // +optional + MaxResidentSessions int32 `json:"maxResidentSessions,omitempty"` + + // MaxRunningPrompts is the maximum number of concurrently running prompts. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=1000 + // +kubebuilder:default=4 + // +optional + MaxRunningPrompts int32 `json:"maxRunningPrompts,omitempty"` +} + +// RuntimePoolSpec defines the desired state of a controller-owned logical pool. +// Trust-domain placement and the runtime image/profile are immutable; rollout +// uses drain-and-replace rather than changing an in-memory instance in place. +// +kubebuilder:validation:XValidation:rule="self.trustDomain == oldSelf.trustDomain",message="trustDomain is immutable" +// +kubebuilder:validation:XValidation:rule="has(self.runtimeNamespace) == has(oldSelf.runtimeNamespace) && (!has(self.runtimeNamespace) || self.runtimeNamespace == oldSelf.runtimeNamespace)",message="runtimeNamespace is immutable" +// +kubebuilder:validation:XValidation:rule="self.runtime == oldSelf.runtime",message="runtime image and profile are immutable" +type RuntimePoolSpec struct { + // TrustDomain is the logical namespace/identity boundary served by this pool. + // +kubebuilder:validation:Required + TrustDomain RuntimePoolTrustDomain `json:"trustDomain"` + + // RuntimeNamespace is the physical namespace for controller-owned runtime + // resources. When omitted, the controller selects its configured runtime namespace. + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + // +optional + RuntimeNamespace string `json:"runtimeNamespace,omitempty"` + + // Runtime pins the immutable supervisor image and behavior profile. + // +kubebuilder:validation:Required + Runtime RuntimePoolRuntimeSpec `json:"runtime"` + + // DesiredReplicas is zero or one. More than one runtime Pod would make + // stateful exact-instance routing ambiguous. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + // +kubebuilder:default=0 + // +optional + DesiredReplicas int32 `json:"desiredReplicas,omitempty"` + + // Capacity sets resident-session and running-prompt limits. + // +kubebuilder:default={maxResidentSessions:10,maxRunningPrompts:4} + // +optional + Capacity *RuntimePoolCapacitySpec `json:"capacity,omitempty"` + + // ColdStartTimeoutSeconds bounds a 0 -> 1 startup before the pool is marked degraded. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=3600 + // +kubebuilder:default=120 + // +optional + ColdStartTimeoutSeconds int32 `json:"coldStartTimeoutSeconds,omitempty"` +} + +// RuntimePoolActiveInstanceStatus is the exact selected runtime Pod and +// supervisor boot. Every stateful request is fenced to this identity. +type RuntimePoolActiveInstanceStatus struct { + // PodNamespace is the namespace containing the selected runtime Pod. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + PodNamespace string `json:"podNamespace"` + + // PodName is the exact selected runtime Pod name. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + PodName string `json:"podName"` + + // PodAddress is the exact Pod address used for stateful routing, not a + // load-balanced Service endpoint. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + PodAddress string `json:"podAddress"` + + // PodUID is the Kubernetes UID of the selected Pod. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + PodUID string `json:"podUID"` + + // BootID is the immutable supervisor boot identifier inside the selected Pod. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + BootID string `json:"bootID"` + + // RuntimeInstanceID is the portable v2 instance fence derived from PodUID and BootID. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + RuntimeInstanceID string `json:"runtimeInstanceID"` + + // ControllerEpoch is the durable controller epoch to which this instance is bound. + // +kubebuilder:validation:Minimum=1 + ControllerEpoch int64 `json:"controllerEpoch"` + + // ProtocolVersion is the supervisor protocol actually advertised by this instance. + // +kubebuilder:validation:Required + ProtocolVersion RuntimePoolProtocolVersion `json:"protocolVersion"` + + // ProfileDigest is the immutable runtime-profile digest advertised by this instance. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + ProfileDigest string `json:"profileDigest"` + + // ProfileDigestSchemaVersion is the digest schema advertised by this instance. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + ProfileDigestSchemaVersion string `json:"profileDigestSchemaVersion"` + + // ProviderTokenGeneration is a non-secret digest generation for the exact + // provider capability mounted into this runtime Pod. It lets the controller + // prove that a selected instance converged on the intended proxy credential + // without exposing the bearer token. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^[a-f0-9]{16}$` + ProviderTokenGeneration string `json:"providerTokenGeneration"` + + // LastObservedTime is the last authenticated status observation for this instance. + // +optional + LastObservedTime *metav1.Time `json:"lastObservedTime,omitempty"` +} + +// RuntimePoolCapacityReservationStatus is one durable, exact-instance capacity +// claim. The composite key is the pool UID, Task UID, attempt, and controller +// epoch. A reservation claims resident-session and prompt admission slots until +// the supervisor accepts the corresponding work or the reservation expires. +// +kubebuilder:validation:XValidation:rule="self.residentSlots + self.promptSlots > 0",message="a capacity reservation must claim at least one slot" +type RuntimePoolCapacityReservationStatus struct { + // PoolUID fences the claim to the exact RuntimePool object. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + PoolUID string `json:"poolUID"` + + // TaskUID is the immutable Task identity that owns the claim. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + TaskUID string `json:"taskUID"` + + // Attempt is the Task attempt that owns the claim. + // +kubebuilder:validation:Minimum=1 + Attempt int32 `json:"attempt"` + + // ControllerEpoch fences the claim to one controller leadership epoch. + // +kubebuilder:validation:Minimum=1 + ControllerEpoch int64 `json:"controllerEpoch"` + + // RuntimeInstanceID binds admission to the exact selected Pod/boot pair. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + RuntimeInstanceID string `json:"runtimeInstanceID"` + + // ResidentSlots is zero after a RuntimeSession is admitted and one while a + // new resident-session slot is still reserved. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + ResidentSlots int32 `json:"residentSlots"` + + // PromptSlots is one until the prompt is accepted by the supervisor. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + PromptSlots int32 `json:"promptSlots"` + + // ReservedAt is the first successful resource-version CAS for this claim. + // +kubebuilder:validation:Required + ReservedAt metav1.Time `json:"reservedAt"` + + // ExpiresAt is renewed while pre-admission work is active. A later + // dispatcher may reclaim the claim after this time. + // +kubebuilder:validation:Required + ExpiresAt metav1.Time `json:"expiresAt"` +} + +// RuntimePoolCapacityStatus reports controller and authenticated-supervisor +// capacity counters. These counters are advisory unless fenced by the current +// ActiveInstance and controller epoch. Reservation records are authoritative for +// coordinator-owned pre-admission capacity. +type RuntimePoolCapacityStatus struct { + // MaxResidentSessions is the effective configured resident-session limit. + // +kubebuilder:validation:Minimum=0 + MaxResidentSessions int32 `json:"maxResidentSessions,omitempty"` + + // MaxRunningPrompts is the effective configured running-prompt limit. + // +kubebuilder:validation:Minimum=0 + MaxRunningPrompts int32 `json:"maxRunningPrompts,omitempty"` + + // ResidentSessions is the authenticated supervisor count of resident sessions. + // +kubebuilder:validation:Minimum=0 + ResidentSessions int32 `json:"residentSessions,omitempty"` + + // RunningPrompts is the authenticated supervisor count of active prompts. + // +kubebuilder:validation:Minimum=0 + RunningPrompts int32 `json:"runningPrompts,omitempty"` + + // QueuedTasks is durable unsatisfied demand assigned to this pool. + // +kubebuilder:validation:Minimum=0 + QueuedTasks int32 `json:"queuedTasks,omitempty"` + + // ReservedSessions is the sum of resident slots in Reservations. + // +kubebuilder:validation:Minimum=0 + ReservedSessions int32 `json:"reservedSessions,omitempty"` + + // ReservedPrompts is the sum of prompt slots in Reservations. + // +kubebuilder:validation:Minimum=0 + ReservedPrompts int32 `json:"reservedPrompts,omitempty"` + + // Reservations is the bounded authoritative set of coordinator-owned + // pre-admission capacity claims. + // +listType=map + // +listMapKey=poolUID + // +listMapKey=taskUID + // +listMapKey=attempt + // +listMapKey=controllerEpoch + // +kubebuilder:validation:MaxItems=1000 + // +optional + Reservations []RuntimePoolCapacityReservationStatus `json:"reservations,omitempty"` + + // PendingPermissions is the authenticated count of unresolved prompt permissions. + // +kubebuilder:validation:Minimum=0 + PendingPermissions int32 `json:"pendingPermissions,omitempty"` + + // FinalizingSessions is the count reserved for validation, publication, or finalization. + // +kubebuilder:validation:Minimum=0 + FinalizingSessions int32 `json:"finalizingSessions,omitempty"` + + // LiveDescendants is the authenticated count of tracked runtime descendants. + // +kubebuilder:validation:Minimum=0 + LiveDescendants int32 `json:"liveDescendants,omitempty"` +} + +// RuntimePoolStatus defines the observed state of a controller-owned pool. +type RuntimePoolStatus struct { + // ObservedGeneration is the latest RuntimePool generation reconciled by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // ControllerEpoch is the durable epoch required for authoritative pool writes. + // +kubebuilder:validation:Minimum=0 + // +optional + ControllerEpoch int64 `json:"controllerEpoch,omitempty"` + + // DesiredReplicas is the desired replica count observed by the controller. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + // +optional + DesiredReplicas int32 `json:"desiredReplicas,omitempty"` + + // CurrentReplicas is the number of non-terminated runtime Pods owned by the pool. + // +kubebuilder:validation:Minimum=0 + // +optional + CurrentReplicas int32 `json:"currentReplicas,omitempty"` + + // Lifecycle is the explicit pool lifecycle. + // +optional + Lifecycle RuntimePoolLifecycle `json:"lifecycle,omitempty"` + + // AdmissionState is the authoritative admission gate for new RuntimeSessions. + // +optional + AdmissionState RuntimePoolAdmissionState `json:"admissionState,omitempty"` + + // ActiveInstance is the exact selected Pod and supervisor boot. It is empty + // unless one instance has been authoritatively selected. + // +optional + ActiveInstance *RuntimePoolActiveInstanceStatus `json:"activeInstance,omitempty"` + + // Capacity reports effective limits, use, and queued demand. + // +optional + Capacity RuntimePoolCapacityStatus `json:"capacity,omitempty"` + + // Message contains bounded, sanitized reconciliation context. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Message string `json:"message,omitempty"` + + // Conditions report admission, Pod Security, quota, scheduling, rollout, and + // other controller-observed failures. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=rtpool +// +kubebuilder:printcolumn:name="Lifecycle",type=string,JSONPath=`.status.lifecycle` +// +kubebuilder:printcolumn:name="Admission",type=string,JSONPath=`.status.admissionState` +// +kubebuilder:printcolumn:name="Desired",type=integer,JSONPath=`.status.desiredReplicas` +// +kubebuilder:printcolumn:name="Current",type=integer,JSONPath=`.status.currentReplicas` +// +kubebuilder:printcolumn:name="Sessions",type=integer,JSONPath=`.status.capacity.residentSessions` +// +kubebuilder:printcolumn:name="Prompts",type=integer,JSONPath=`.status.capacity.runningPrompts` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// RuntimePool is the Schema for controller-owned ACP runtime pools. +type RuntimePool struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RuntimePoolSpec `json:"spec,omitempty"` + Status RuntimePoolStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RuntimePoolList contains a list of RuntimePool. +type RuntimePoolList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RuntimePool `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RuntimePool{}, &RuntimePoolList{}) +} diff --git a/api/v1alpha1/runtime_pool_types_test.go b/api/v1alpha1/runtime_pool_types_test.go new file mode 100644 index 000000000..12eac9252 --- /dev/null +++ b/api/v1alpha1/runtime_pool_types_test.go @@ -0,0 +1,183 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import ( + "encoding/json" + "strings" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestRuntimePoolDefaultConstants(t *testing.T) { + if DefaultRuntimePoolDesiredReplicas != 0 { + t.Fatalf("DefaultRuntimePoolDesiredReplicas = %d, want 0", DefaultRuntimePoolDesiredReplicas) + } + if DefaultRuntimePoolMaxResidentSessions != 10 { + t.Fatalf("DefaultRuntimePoolMaxResidentSessions = %d, want 10", DefaultRuntimePoolMaxResidentSessions) + } + if DefaultRuntimePoolMaxRunningPrompts != 4 { + t.Fatalf("DefaultRuntimePoolMaxRunningPrompts = %d, want 4", DefaultRuntimePoolMaxRunningPrompts) + } + if DefaultRuntimePoolColdStartTimeoutSeconds != 120 { + t.Fatalf("DefaultRuntimePoolColdStartTimeoutSeconds = %d, want 120", DefaultRuntimePoolColdStartTimeoutSeconds) + } +} + +func TestRuntimePoolLifecycleConstants(t *testing.T) { + tests := []struct { + got RuntimePoolLifecycle + want string + }{ + {RuntimePoolLifecycleStopped, "Stopped"}, + {RuntimePoolLifecycleStarting, "Starting"}, + {RuntimePoolLifecycleServing, "Serving"}, + {RuntimePoolLifecycleDraining, "Draining"}, + {RuntimePoolLifecycleQuiescent, "Quiescent"}, + {RuntimePoolLifecycleStopping, "Stopping"}, + {RuntimePoolLifecycleDegraded, "Degraded"}, + {RuntimePoolLifecycleAmbiguous, "Ambiguous"}, + } + for _, tt := range tests { + if string(tt.got) != tt.want { + t.Errorf("RuntimePoolLifecycle = %q, want %q", tt.got, tt.want) + } + } +} + +func TestRuntimePoolFieldsRoundTrip(t *testing.T) { + digest := "sha256:" + strings.Repeat("a", 64) + pool := RuntimePool{ + TypeMeta: metav1.TypeMeta{ + APIVersion: GroupVersion.String(), + Kind: "RuntimePool", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "codex-default", + Namespace: "tenant-a", + }, + Spec: RuntimePoolSpec{ + TrustDomain: RuntimePoolTrustDomain{ + Namespace: "tenant-a", + Identity: "tenant-a/default", + }, + RuntimeNamespace: "orka-runtimes", + Runtime: RuntimePoolRuntimeSpec{ + Image: "docker.io/example/acp-runtime@" + digest, + Profile: RuntimePoolProfileSpec{ + ProtocolVersion: RuntimePoolProtocolHarnessV2, + Digest: digest, + DigestSchemaVersion: "v1", + ResourceClass: "standard", + }, + }, + DesiredReplicas: 1, + Capacity: &RuntimePoolCapacitySpec{ + MaxResidentSessions: 10, + MaxRunningPrompts: 4, + }, + ColdStartTimeoutSeconds: 120, + }, + Status: RuntimePoolStatus{ + ObservedGeneration: 3, + ControllerEpoch: 9, + DesiredReplicas: 1, + CurrentReplicas: 1, + Lifecycle: RuntimePoolLifecycleServing, + AdmissionState: RuntimePoolAdmissionAccepting, + ActiveInstance: &RuntimePoolActiveInstanceStatus{ + PodNamespace: "orka-runtimes", + PodName: "codex-default-0", + PodAddress: "10.0.0.42", + PodUID: "11111111-2222-3333-4444-555555555555", + BootID: "boot-1", + RuntimeInstanceID: "runtime-instance-1", + ControllerEpoch: 9, + ProtocolVersion: RuntimePoolProtocolHarnessV2, + ProfileDigest: digest, + ProfileDigestSchemaVersion: "v1", + }, + Capacity: RuntimePoolCapacityStatus{ + MaxResidentSessions: 10, + MaxRunningPrompts: 4, + ResidentSessions: 3, + RunningPrompts: 2, + QueuedTasks: 5, + ReservedSessions: 1, + ReservedPrompts: 1, + Reservations: []RuntimePoolCapacityReservationStatus{{ + PoolUID: "pool-uid", TaskUID: "task-uid", Attempt: 1, ControllerEpoch: 9, + RuntimeInstanceID: "runtime-instance-1", ResidentSlots: 1, PromptSlots: 1, + ReservedAt: metav1.NewTime(time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)), + ExpiresAt: metav1.NewTime(time.Date(2026, 7, 25, 12, 2, 0, 0, time.UTC)), + }}, + }, + Conditions: []metav1.Condition{{ + Type: RuntimePoolConditionAdmissionReady, + Status: "True", + Reason: "Serving", + }}, + }, + } + + encoded, err := json.Marshal(&pool) + if err != nil { + t.Fatalf("json.Marshal(RuntimePool): %v", err) + } + var decoded RuntimePool + if err := json.Unmarshal(encoded, &decoded); err != nil { + t.Fatalf("json.Unmarshal(RuntimePool): %v", err) + } + + if decoded.Spec.TrustDomain.Identity != pool.Spec.TrustDomain.Identity { + t.Errorf("trust domain identity = %q, want %q", decoded.Spec.TrustDomain.Identity, pool.Spec.TrustDomain.Identity) + } + if decoded.Status.ActiveInstance == nil { + t.Fatal("active instance was lost during JSON round trip") + } + if decoded.Status.ActiveInstance.RuntimeInstanceID != "runtime-instance-1" { + t.Errorf("runtime instance ID = %q, want runtime-instance-1", decoded.Status.ActiveInstance.RuntimeInstanceID) + } + if decoded.Status.Capacity.QueuedTasks != 5 { + t.Errorf("queued tasks = %d, want 5", decoded.Status.Capacity.QueuedTasks) + } + if len(decoded.Status.Capacity.Reservations) != 1 || decoded.Status.Capacity.Reservations[0].TaskUID != "task-uid" || decoded.Status.Capacity.ReservedPrompts != 1 { + t.Fatalf("capacity reservations = %#v", decoded.Status.Capacity) + } +} + +func TestRuntimePoolReservationDeepCopy(t *testing.T) { + pool := &RuntimePool{Status: RuntimePoolStatus{Capacity: RuntimePoolCapacityStatus{ + Reservations: []RuntimePoolCapacityReservationStatus{{ + PoolUID: "pool-uid", TaskUID: "task-uid", Attempt: 1, ControllerEpoch: 1, RuntimeInstanceID: "instance", + ResidentSlots: 1, PromptSlots: 1, ReservedAt: metav1.Now(), ExpiresAt: metav1.Now(), + }}, + }}} + copy := pool.DeepCopy() + copy.Status.Capacity.Reservations[0].TaskUID = "changed" + if pool.Status.Capacity.Reservations[0].TaskUID != "task-uid" { + t.Fatal("RuntimePool DeepCopy aliased capacity reservations") + } +} + +func TestRuntimePoolRegisteredWithScheme(t *testing.T) { + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("AddToScheme: %v", err) + } + + obj, err := scheme.New(GroupVersion.WithKind("RuntimePool")) + if err != nil { + t.Fatalf("scheme.New(RuntimePool): %v", err) + } + if _, ok := obj.(*RuntimePool); !ok { + t.Fatalf("scheme.New(RuntimePool) returned %T", obj) + } +} diff --git a/api/v1alpha1/runtime_session_control_types.go b/api/v1alpha1/runtime_session_control_types.go new file mode 100644 index 000000000..9a9e30c8d --- /dev/null +++ b/api/v1alpha1/runtime_session_control_types.go @@ -0,0 +1,172 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// RuntimeSessionControlLifecycle is the durable RuntimeSession lifecycle. +// +kubebuilder:validation:Enum=Creating;Idle;PromptRunning;Validating;PreparingPublication;PublicationPrepared;Publishing;Verifying;Finalizing;Cancelling;Poisoned;Deleting;Deleted +type RuntimeSessionControlLifecycle string + +// RuntimeSessionControlAvailability gates the Session mutation lease. +// +kubebuilder:validation:Enum=Available;ReconciliationBlocked +type RuntimeSessionControlAvailability string + +// RuntimeSessionControlSpec contains immutable session identity, ownership, and +// profile bindings. Profile changes create a new session generation in status; +// they do not mutate this immutable record identity. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="runtime session control spec is immutable" +type RuntimeSessionControlSpec struct { + // SessionName is the immutable user-visible Session key within the object + // namespace. The Kubernetes object name is a digest-derived storage key and + // must not be treated as the Session name. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + SessionName string `json:"sessionName"` + + // SessionUID is the immutable Orka Session identity. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + SessionUID string `json:"sessionUid"` + + // RequestDigest binds creation to exact canonical input. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` + + // Owner identifies the immutable Task or durable Session owner. + Owner ControlRecordOwner `json:"owner"` + + // RuntimePoolRef is the controller-owned logical pool name when known. + // +optional + // +kubebuilder:validation:MaxLength=253 + RuntimePoolRef string `json:"runtimePoolRef,omitempty"` + + // RuntimePoolUID fences the pool object across delete/recreate. + // +optional + // +kubebuilder:validation:MaxLength=1024 + RuntimePoolUID string `json:"runtimePoolUid,omitempty"` + + // RuntimeProfileDigest binds the session to immutable runtime behavior. + // +optional + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RuntimeProfileDigest string `json:"runtimeProfileDigest,omitempty"` + + // ProfileDigestSchemaVersion identifies how RuntimeProfileDigest was built. + // +optional + // +kubebuilder:validation:MaxLength=64 + ProfileDigestSchemaVersion string `json:"profileDigestSchemaVersion,omitempty"` +} + +// RuntimeSessionMutationLeaseStatus mirrors the namespaced Kubernetes Lease +// that serializes mutation for one immutable SessionUID. +type RuntimeSessionMutationLeaseStatus struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + LeaseName string `json:"leaseName"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + LeaseResourceVersion string `json:"leaseResourceVersion"` + + // +kubebuilder:validation:Minimum=1 + Generation int64 `json:"generation"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + TaskUID string `json:"taskUid"` + + // +kubebuilder:validation:Minimum=1 + Attempt int64 `json:"attempt"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + PromptID string `json:"promptId"` + + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + RequestDigest string `json:"requestDigest"` + + AcquiredAt metav1.Time `json:"acquiredAt"` + + // +optional + ExpiresAt *metav1.Time `json:"expiresAt,omitempty"` +} + +// RuntimeSessionControlStatus contains the lifecycle, generation, mutation +// Lease, and independently verified recovery baseline. +// +kubebuilder:validation:XValidation:rule="!has(self.availability) || self.availability != 'Available' || ((!has(self.blockedReason) || size(self.blockedReason) == 0) && (!has(self.relatedPromptAttemptId) || size(self.relatedPromptAttemptId) == 0) && (!has(self.relatedPublicationId) || size(self.relatedPublicationId) == 0))",message="available sessions must clear reconciliation block metadata" +// +kubebuilder:validation:XValidation:rule="!has(self.availability) || self.availability != 'ReconciliationBlocked' || (has(self.blockedReason) && size(self.blockedReason) > 0)",message="reconciliation-blocked sessions require a reason" +type RuntimeSessionControlStatus struct { + // Generation is the monotonic ACP RuntimeSession generation. + // +optional + // +kubebuilder:validation:Minimum=1 + Generation int64 `json:"generation,omitempty"` + + // +optional + Lifecycle RuntimeSessionControlLifecycle `json:"lifecycle,omitempty"` + + // +optional + Availability RuntimeSessionControlAvailability `json:"availability,omitempty"` + + // MutationLeaseGeneration is monotonic and never reused for SessionUID. + // +optional + // +kubebuilder:validation:Minimum=0 + MutationLeaseGeneration int64 `json:"mutationLeaseGeneration,omitempty"` + + // +optional + MutationLease *RuntimeSessionMutationLeaseStatus `json:"mutationLease,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=16384 + BlockedReason string `json:"blockedReason,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=1024 + RelatedPromptAttemptID string `json:"relatedPromptAttemptId,omitempty"` + + // +optional + // +kubebuilder:validation:MaxLength=1024 + RelatedPublicationID string `json:"relatedPublicationId,omitempty"` + + // +optional + VerifiedBaseline *ControlVerifiedBranchBaseline `json:"verifiedBaseline,omitempty"` + + ControlRecordMutationStatus `json:",inline"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=rsctrl +// +kubebuilder:printcolumn:name="Lifecycle",type=string,JSONPath=`.status.lifecycle` +// +kubebuilder:printcolumn:name="Availability",type=string,JSONPath=`.status.availability` +// +kubebuilder:printcolumn:name="Generation",type=integer,JSONPath=`.status.generation` +// +kubebuilder:printcolumn:name="Lease",type=integer,JSONPath=`.status.mutationLeaseGeneration` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// RuntimeSessionControl is the Kubernetes-authoritative RuntimeSession control +// record. SessionTurn/transcript/deferred-outbox data remains in one durable +// SQLite transaction; the Kubernetes store completes the authoritative +// SessionControl/BranchClaim CAS before activating the terminal projection. +type RuntimeSessionControl struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RuntimeSessionControlSpec `json:"spec"` + Status RuntimeSessionControlStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RuntimeSessionControlList contains a list of RuntimeSessionControl. +type RuntimeSessionControlList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RuntimeSessionControl `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RuntimeSessionControl{}, &RuntimeSessionControlList{}) +} diff --git a/api/v1alpha1/task_runtime_types.go b/api/v1alpha1/task_runtime_types.go new file mode 100644 index 000000000..bde4676ca --- /dev/null +++ b/api/v1alpha1/task_runtime_types.go @@ -0,0 +1,432 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// WorkspaceIntent declares whether an agent workspace is immutable read-only +// input or may produce a validated publication artifact. Agent Tasks that omit +// intent are interpreted as read by controller logic; container Task behavior +// is unchanged when intent is omitted. +// +kubebuilder:validation:Enum=read;write +type WorkspaceIntent string + +const ( + WorkspaceIntentRead WorkspaceIntent = "read" + WorkspaceIntentWrite WorkspaceIntent = "write" +) + +// WorkspaceCredentialReference references a Secret in the Task namespace. The +// controller freezes the Secret resourceVersion when it reserves an attempt; +// Secret contents are never copied to Task status. +type WorkspaceCredentialReference struct { + // Name is the name of the Secret in the Task namespace. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` + Name string `json:"name"` + + // Key is the Secret data key containing one bearer token or one complete + // Authorization header. It defaults to "token" when omitted. + // +optional + // +kubebuilder:default=token + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[A-Za-z0-9._-]+$` + Key string `json:"key,omitempty"` +} + +// RepositoryIdentity is the canonical identity derived from a credential-free +// repository URL. It is used for ownership, BranchClaim, and publication +// reconciliation decisions without persisting credentials. +type RepositoryIdentity struct { + // Provider identifies the source-control provider or forge. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$` + Provider string `json:"provider"` + + // ID is the canonical credential-free URL identity and must match the + // corresponding repository URL after normalization. For GitHub, use + // "github.com/owner/repo"; GitHub GraphQL node IDs are not accepted. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=512 + ID string `json:"id"` +} + +// TaskExecutionState is the durable execution state of one Task attempt. +// +kubebuilder:validation:Enum=Queued;Reserved;SessionStarting;Planned;Submitting;SubmittedUnknown;Accepted;Running;Settling;Succeeded;Failed;Cancelled;OutcomeUnknown +type TaskExecutionState string + +const ( + TaskExecutionStateQueued TaskExecutionState = "Queued" + TaskExecutionStateReserved TaskExecutionState = "Reserved" + TaskExecutionStateSessionStarting TaskExecutionState = "SessionStarting" + TaskExecutionStatePlanned TaskExecutionState = "Planned" + TaskExecutionStateSubmitting TaskExecutionState = "Submitting" + TaskExecutionStateSubmittedUnknown TaskExecutionState = "SubmittedUnknown" + TaskExecutionStateAccepted TaskExecutionState = "Accepted" + TaskExecutionStateRunning TaskExecutionState = "Running" + TaskExecutionStateSettling TaskExecutionState = "Settling" + TaskExecutionStateSucceeded TaskExecutionState = "Succeeded" + TaskExecutionStateFailed TaskExecutionState = "Failed" + TaskExecutionStateCancelled TaskExecutionState = "Cancelled" + TaskExecutionStateOutcomeUnknown TaskExecutionState = "OutcomeUnknown" +) + +// TaskExecutionOutcome is the terminal classification of one Task attempt. +// OutcomeUnknown is terminal and must never be treated as generic retryable +// failure by controller logic. +// +kubebuilder:validation:Enum=Succeeded;Failed;Cancelled;OutcomeUnknown +type TaskExecutionOutcome string + +const ( + TaskExecutionOutcomeSucceeded TaskExecutionOutcome = "Succeeded" + TaskExecutionOutcomeFailed TaskExecutionOutcome = "Failed" + TaskExecutionOutcomeCancelled TaskExecutionOutcome = "Cancelled" + TaskExecutionOutcomeOutcomeUnknown TaskExecutionOutcome = "OutcomeUnknown" +) + +// TaskExecutionReason is a bounded machine-readable execution reason. +// +kubebuilder:validation:MaxLength=128 +// +kubebuilder:validation:Pattern=`^[A-Za-z][A-Za-z0-9._-]{0,127}$` +type TaskExecutionReason string + +const ( + // TaskExecutionReasonAtCapacity is a scheduling reason, not a terminal Task phase. + TaskExecutionReasonAtCapacity TaskExecutionReason = "AtCapacity" + // TaskExecutionReasonRuntimeLost classifies accepted or running work lost with + // an unprovable terminal result; its outcome must be OutcomeUnknown. + TaskExecutionReasonRuntimeLost TaskExecutionReason = "RuntimeLost" +) + +// TaskExecutionStatus is the structured execution lifecycle for the current +// Task attempt. The existing top-level Task phase remains the compatibility +// projection and is not replaced by this status. +// +kubebuilder:validation:XValidation:rule="!has(self.outcome) || (has(self.state) && self.state == self.outcome)",message="execution outcome requires the matching terminal state" +// +kubebuilder:validation:XValidation:rule="!has(self.state) || !(self.state in ['Succeeded', 'Failed', 'Cancelled', 'OutcomeUnknown']) || has(self.outcome)",message="terminal execution state requires an outcome" +type TaskExecutionStatus struct { + // State is the current durable execution state. + // +optional + State TaskExecutionState `json:"state,omitempty"` + + // Outcome is set only after execution reaches a terminal classification. + // +optional + Outcome TaskExecutionOutcome `json:"outcome,omitempty"` + + // Reason is a stable machine-readable explanation for State or Outcome. + // +optional + Reason TaskExecutionReason `json:"reason,omitempty"` + + // Attempt is the one-based Task execution attempt represented by this status. + // +kubebuilder:validation:Minimum=1 + // +optional + Attempt int32 `json:"attempt,omitempty"` + + // PromptID is the durable prompt identity used for submission and settlement. + // +kubebuilder:validation:MaxLength=253 + // +optional + PromptID string `json:"promptID,omitempty"` + + // RuntimePoolName is the namespaced logical pool selected for this attempt. + // +kubebuilder:validation:MaxLength=253 + // +optional + RuntimePoolName string `json:"runtimePoolName,omitempty"` + + // RuntimePoolUID is the immutable pool UID fenced into runtime requests. + // +kubebuilder:validation:MaxLength=253 + // +optional + RuntimePoolUID string `json:"runtimePoolUID,omitempty"` + + // AgentRuntimeName is the namespaced external orka.harness.v2 registration + // selected for this attempt. It is mutually exclusive with RuntimePoolName. + // +kubebuilder:validation:MaxLength=253 + // +optional + AgentRuntimeName string `json:"agentRuntimeName,omitempty"` + + // AgentRuntimeUID is the immutable external AgentRuntime UID fenced into the + // attempt selection. + // +kubebuilder:validation:MaxLength=253 + // +optional + AgentRuntimeUID string `json:"agentRuntimeUID,omitempty"` + + // RuntimeInstanceID is the exact selected supervisor Pod UID plus boot identity. + // +kubebuilder:validation:MaxLength=512 + // +optional + RuntimeInstanceID string `json:"runtimeInstanceID,omitempty"` + + // RuntimeSessionUID is the stable controller-owned Session execution identity. + // +kubebuilder:validation:MaxLength=253 + // +optional + RuntimeSessionUID string `json:"runtimeSessionUID,omitempty"` + + // RuntimeSessionGeneration is the monotonic profile/session generation. + // +kubebuilder:validation:Minimum=0 + // +optional + RuntimeSessionGeneration int64 `json:"runtimeSessionGeneration,omitempty"` + + // RuntimeSessionSupervisorBootID freezes the supervisor boot that owns a + // pending or reusable Session generation. + // +kubebuilder:validation:MaxLength=512 + // +optional + RuntimeSessionSupervisorBootID string `json:"runtimeSessionSupervisorBootID,omitempty"` + + // RuntimeSessionProfileDigest freezes the immutable runtime behavior bound to + // a pending or reusable Session generation. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + // +optional + RuntimeSessionProfileDigest string `json:"runtimeSessionProfileDigest,omitempty"` + + // RuntimeSessionMCPDigest binds the complete non-secret effective MCP policy + // and descriptor configuration to a pending or reusable Session generation. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + // +optional + RuntimeSessionMCPDigest string `json:"runtimeSessionMCPDigest,omitempty"` + + // RuntimeSessionWorkspaceDigest binds a reusable Session generation to the + // exact repository, source ref, verified baseline, intent, and relative root. + // It contains no credential material. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + // +optional + RuntimeSessionWorkspaceDigest string `json:"runtimeSessionWorkspaceDigest,omitempty"` + + // RuntimeSessionRecreationPending records that the exact generation is being + // created or replaced and must be reconciled before a different request may + // reuse that identity. + // +optional + RuntimeSessionRecreationPending bool `json:"runtimeSessionRecreationPending,omitempty"` + + // RuntimeSessionCleanupDigest is the controller-owned proof that the exact + // RuntimeSession requiring retirement was deleted or its immutable runtime + // instance was replaced. Users may read but cannot mutate the Task status subresource. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + // +optional + RuntimeSessionCleanupDigest string `json:"runtimeSessionCleanupDigest,omitempty"` + + // RequestDigest is the canonical immutable prompt request digest. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + // +optional + RequestDigest string `json:"requestDigest,omitempty"` + + // ControllerEpoch is the durable controller epoch fencing this attempt. + // +kubebuilder:validation:Minimum=0 + // +optional + ControllerEpoch int64 `json:"controllerEpoch,omitempty"` + + // ReadCredentialResourceVersion freezes the read credential Secret version + // selected at reservation without exposing credential material. + // +optional + // +kubebuilder:validation:MaxLength=253 + ReadCredentialResourceVersion string `json:"readCredentialResourceVersion,omitempty"` + + // PublicationReadCredentialResourceVersion freezes the target-read Secret + // version used for preflight and independent verification. + // +optional + // +kubebuilder:validation:MaxLength=253 + PublicationReadCredentialResourceVersion string `json:"publicationReadCredentialResourceVersion,omitempty"` + + // PublicationCredentialResourceVersion freezes the target-write Secret + // version selected at reservation without exposing credential material. + // +optional + // +kubebuilder:validation:MaxLength=253 + PublicationCredentialResourceVersion string `json:"publicationCredentialResourceVersion,omitempty"` + + // ForgeCredentialResourceVersion freezes the forge-only Secret version used + // for pull request reconciliation. + // +optional + // +kubebuilder:validation:MaxLength=253 + ForgeCredentialResourceVersion string `json:"forgeCredentialResourceVersion,omitempty"` + + // Message contains bounded, sanitized execution context. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Message string `json:"message,omitempty"` + + // LastTransitionTime is the last durable execution-state transition time. + // +optional + LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"` +} + +// TaskDeliveryState is the durable validation and publication lifecycle. +// +kubebuilder:validation:Enum=NotRequested;Validating;Preparing;Prepared;Publishing;Verifying;VerifiedExact;DeliveredSuperseded;ReadValidated;NoChange;CancelledBeforePublish;ReadOnlyWorkspaceModified;DeliveryConflict;CredentialBlocked;PublicationOutcomeUnknown +type TaskDeliveryState string + +const ( + TaskDeliveryStateNotRequested TaskDeliveryState = "NotRequested" + TaskDeliveryStateValidating TaskDeliveryState = "Validating" + TaskDeliveryStatePreparing TaskDeliveryState = "Preparing" + TaskDeliveryStatePrepared TaskDeliveryState = "Prepared" + TaskDeliveryStatePublishing TaskDeliveryState = "Publishing" + TaskDeliveryStateVerifying TaskDeliveryState = "Verifying" + TaskDeliveryStateVerifiedExact TaskDeliveryState = "VerifiedExact" + TaskDeliveryStateDeliveredSuperseded TaskDeliveryState = "DeliveredSuperseded" + TaskDeliveryStateReadValidated TaskDeliveryState = "ReadValidated" + TaskDeliveryStateNoChange TaskDeliveryState = "NoChange" + TaskDeliveryStateCancelledBeforePublish TaskDeliveryState = "CancelledBeforePublish" + TaskDeliveryStateReadOnlyWorkspaceModified TaskDeliveryState = "ReadOnlyWorkspaceModified" + TaskDeliveryStateDeliveryConflict TaskDeliveryState = "DeliveryConflict" + TaskDeliveryStateCredentialBlocked TaskDeliveryState = "CredentialBlocked" + TaskDeliveryStatePublicationOutcomeUnknown TaskDeliveryState = "PublicationOutcomeUnknown" +) + +// TaskDeliveryOutcome is the terminal delivery classification. +// +kubebuilder:validation:Enum=NotRequested;VerifiedExact;DeliveredSuperseded;ReadValidated;NoChange;CancelledBeforePublish;ReadOnlyWorkspaceModified;DeliveryConflict;CredentialBlocked;PublicationOutcomeUnknown +type TaskDeliveryOutcome string + +const ( + TaskDeliveryOutcomeNotRequested TaskDeliveryOutcome = "NotRequested" + TaskDeliveryOutcomeVerifiedExact TaskDeliveryOutcome = "VerifiedExact" + TaskDeliveryOutcomeDeliveredSuperseded TaskDeliveryOutcome = "DeliveredSuperseded" + TaskDeliveryOutcomeReadValidated TaskDeliveryOutcome = "ReadValidated" + TaskDeliveryOutcomeNoChange TaskDeliveryOutcome = "NoChange" + TaskDeliveryOutcomeCancelledBeforePublish TaskDeliveryOutcome = "CancelledBeforePublish" + TaskDeliveryOutcomeReadOnlyWorkspaceModified TaskDeliveryOutcome = "ReadOnlyWorkspaceModified" + TaskDeliveryOutcomeDeliveryConflict TaskDeliveryOutcome = "DeliveryConflict" + TaskDeliveryOutcomeCredentialBlocked TaskDeliveryOutcome = "CredentialBlocked" + TaskDeliveryOutcomePublicationOutcomeUnknown TaskDeliveryOutcome = "PublicationOutcomeUnknown" +) + +// TaskDeliveryReason is a bounded machine-readable delivery reason. +// +kubebuilder:validation:MaxLength=128 +// +kubebuilder:validation:Pattern=`^[A-Za-z][A-Za-z0-9._-]{0,127}$` +type TaskDeliveryReason string + +const ( + // TaskDeliveryReasonCancellationRequestedAfterPublish records that publication + // won the durable CAS and must continue reconciliation despite cancellation. + TaskDeliveryReasonCancellationRequestedAfterPublish TaskDeliveryReason = "CancellationRequestedAfterPublish" +) + +// TaskPullRequestReceipt is the bounded, non-secret receipt for an explicitly +// requested pull request reconciliation. +type TaskPullRequestReceipt struct { + // ID is the provider's durable pull request identifier. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=512 + ID string `json:"id"` + + // Number is the provider's numeric pull request number when available. + // +kubebuilder:validation:Minimum=1 + // +optional + Number int64 `json:"number,omitempty"` + + // URL is the canonical user-facing pull request URL. It must not contain credentials. + // +kubebuilder:validation:MaxLength=2048 + // +optional + URL string `json:"url,omitempty"` + + // State is the provider-observed pull request state. + // +kubebuilder:validation:MaxLength=64 + // +optional + State string `json:"state,omitempty"` + + // BaseBranch is the reconciled pull request base branch. + // +kubebuilder:validation:MaxLength=255 + // +optional + BaseBranch string `json:"baseBranch,omitempty"` + + // HeadBranch is the reconciled pull request head branch. + // +kubebuilder:validation:MaxLength=255 + // +optional + HeadBranch string `json:"headBranch,omitempty"` + + // HeadSHA is the exact observed pull request head commit. + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + HeadSHA string `json:"headSHA,omitempty"` +} + +// TaskDeliveryStatus is the structured validation/publication status and its +// durable, non-secret receipt. +// +kubebuilder:validation:XValidation:rule="!has(self.outcome) || (has(self.state) && self.state == self.outcome)",message="delivery outcome requires the matching terminal state" +// +kubebuilder:validation:XValidation:rule="!has(self.state) || !(self.state in ['NotRequested', 'VerifiedExact', 'DeliveredSuperseded', 'ReadValidated', 'NoChange', 'CancelledBeforePublish', 'ReadOnlyWorkspaceModified', 'DeliveryConflict', 'CredentialBlocked', 'PublicationOutcomeUnknown']) || has(self.outcome)",message="terminal delivery state requires an outcome" +type TaskDeliveryStatus struct { + // State is the current durable delivery state. + // +optional + State TaskDeliveryState `json:"state,omitempty"` + + // Outcome is set only after delivery reaches a terminal classification. + // +optional + Outcome TaskDeliveryOutcome `json:"outcome,omitempty"` + + // Reason is a stable machine-readable explanation for State or Outcome. + // +optional + Reason TaskDeliveryReason `json:"reason,omitempty"` + + // PublicationID is the durable identity reused for reconciliation of the same artifact. + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[A-Za-z0-9][A-Za-z0-9._:-]{0,252}$` + // +optional + PublicationID string `json:"publicationID,omitempty"` + + // SourceRepository is the canonical repository from which the workspace baseline was created. + // +optional + SourceRepository *RepositoryIdentity `json:"sourceRepository,omitempty"` + + // PublicationRepository is the canonical repository whose branch was reconciled. + // +optional + PublicationRepository *RepositoryIdentity `json:"publicationRepository,omitempty"` + + // Branch is the publication branch without a refs/heads/ prefix. + // +kubebuilder:validation:MaxLength=255 + // +optional + Branch string `json:"branch,omitempty"` + + // StartingSHA is the verified source baseline before prompt execution. + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + StartingSHA string `json:"startingSHA,omitempty"` + + // RemoteBeforeSHA is the exact publication ref observed before the CAS push. + // Nil means not yet observed; a pointer to the empty string records explicit + // absence; a non-empty value records the observed object ID. + // +kubebuilder:validation:Pattern=`^(|[a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + RemoteBeforeSHA *string `json:"remoteBeforeSHA,omitempty"` + + // TreeSHA is the deterministic clean-room tree written by the publisher. + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + TreeSHA string `json:"treeSHA,omitempty"` + + // ExpectedCommitSHA is the exact Orka-owned commit prepared for publication. + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + ExpectedCommitSHA string `json:"expectedCommitSHA,omitempty"` + + // VerifiedRemoteSHA is the independently observed remote branch head. + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + VerifiedRemoteSHA string `json:"verifiedRemoteSHA,omitempty"` + + // SupersedingRemoteSHA is the verified descendant that superseded ExpectedCommitSHA. + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + SupersedingRemoteSHA string `json:"supersedingRemoteSHA,omitempty"` + + // ArtifactDigest is the durable content-addressed workspace delta digest. + // +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$` + // +optional + ArtifactDigest string `json:"artifactDigest,omitempty"` + + // PRReceipt is present only when createPR was explicitly requested and reconciled. + // +optional + PRReceipt *TaskPullRequestReceipt `json:"prReceipt,omitempty"` + + // Message contains bounded, sanitized delivery context. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Message string `json:"message,omitempty"` + + // LastTransitionTime is the last durable delivery-state transition time. + // +optional + LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"` +} diff --git a/api/v1alpha1/task_runtime_types_test.go b/api/v1alpha1/task_runtime_types_test.go new file mode 100644 index 000000000..acc1ed946 --- /dev/null +++ b/api/v1alpha1/task_runtime_types_test.go @@ -0,0 +1,187 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestTaskWorkspaceSchemaFields(t *testing.T) { + workspace := WorkspaceConfig{ + Intent: WorkspaceIntentWrite, + GitRepo: "https://github.com/example/source.git", + SourceRepository: &RepositoryIdentity{ + Provider: "github", + ID: "github.com/example/source", + }, + Branch: "main", + Ref: "0123456789abcdef0123456789abcdef01234567", + ReadCredentialRef: &WorkspaceCredentialReference{ + Name: "source-read", + }, + PublicationGitRepo: "https://github.com/example/fork.git", + PublicationRepository: &RepositoryIdentity{ + Provider: "github", + ID: "github.com/example/fork", + }, + PublicationReadCredentialRef: &WorkspaceCredentialReference{Name: "publication-read"}, + PublicationCredentialRef: &WorkspaceCredentialReference{ + Name: "publication-write", + }, + ForgeCredentialRef: &WorkspaceCredentialReference{Name: "forge-token"}, + SubPath: "src/app", + PRBaseBranch: "main", + PushBranch: "orka/task-full-uid", + CreatePR: true, + } + task := TaskSpec{Type: TaskTypeAgent, Workspace: &workspace} + + encoded, err := json.Marshal(task) + if err != nil { + t.Fatalf("json.Marshal(TaskSpec): %v", err) + } + var fields map[string]any + if err := json.Unmarshal(encoded, &fields); err != nil { + t.Fatalf("json.Unmarshal(TaskSpec): %v", err) + } + workspaceJSON, ok := fields["workspace"].(map[string]any) + if !ok { + t.Fatalf("workspace JSON = %#v, want object", fields["workspace"]) + } + for _, field := range []string{ + "intent", + "gitRepo", + "sourceRepository", + "readCredentialRef", + "publicationGitRepo", + "publicationRepository", + "publicationReadCredentialRef", + "publicationCredentialRef", + "forgeCredentialRef", + "createPR", + } { + if _, ok := workspaceJSON[field]; !ok { + t.Errorf("workspace JSON missing %q: %s", field, encoded) + } + } + if _, ok := fields["agentRuntime"]; ok { + t.Fatalf("new top-level workspace unexpectedly serialized agentRuntime: %s", encoded) + } +} + +func TestTaskStructuredExecutionAndDeliveryStatus(t *testing.T) { + sha := strings.Repeat("a", 40) + remoteBeforeSHA := sha + digest := "sha256:" + strings.Repeat("b", 64) + status := TaskStatus{ + Phase: TaskPhaseSucceeded, + Execution: &TaskExecutionStatus{ + State: TaskExecutionStateSucceeded, + Outcome: TaskExecutionOutcomeSucceeded, + Attempt: 1, + PromptID: "prompt-1", + ControllerEpoch: 7, + }, + Delivery: &TaskDeliveryStatus{ + State: TaskDeliveryStateVerifiedExact, + Outcome: TaskDeliveryOutcomeVerifiedExact, + PublicationID: "publication-1", + SourceRepository: &RepositoryIdentity{ + Provider: "github", + ID: "github.com/example/source", + }, + PublicationRepository: &RepositoryIdentity{ + Provider: "github", + ID: "github.com/example/fork", + }, + Branch: "orka/task-full-uid", + StartingSHA: sha, + RemoteBeforeSHA: &remoteBeforeSHA, + TreeSHA: sha, + ExpectedCommitSHA: sha, + VerifiedRemoteSHA: sha, + ArtifactDigest: digest, + PRReceipt: &TaskPullRequestReceipt{ + ID: "PR_1", + Number: 42, + URL: "https://github.com/example/source/pull/42", + State: "open", + BaseBranch: "main", + HeadBranch: "orka/task-full-uid", + HeadSHA: sha, + }, + }, + } + + encoded, err := json.Marshal(status) + if err != nil { + t.Fatalf("json.Marshal(TaskStatus): %v", err) + } + var decoded TaskStatus + if err := json.Unmarshal(encoded, &decoded); err != nil { + t.Fatalf("json.Unmarshal(TaskStatus): %v", err) + } + if decoded.Execution == nil || decoded.Execution.Outcome != TaskExecutionOutcomeSucceeded { + t.Fatalf("execution status round trip = %#v", decoded.Execution) + } + if decoded.Delivery == nil || decoded.Delivery.Outcome != TaskDeliveryOutcomeVerifiedExact { + t.Fatalf("delivery status round trip = %#v", decoded.Delivery) + } + if decoded.Delivery.PRReceipt == nil || decoded.Delivery.PRReceipt.Number != 42 { + t.Fatalf("PR receipt round trip = %#v", decoded.Delivery.PRReceipt) + } +} + +func TestTaskExecutionAndDeliveryOutcomeConstants(t *testing.T) { + execution := map[TaskExecutionOutcome]string{ + TaskExecutionOutcomeSucceeded: "Succeeded", + TaskExecutionOutcomeFailed: "Failed", + TaskExecutionOutcomeCancelled: "Cancelled", + TaskExecutionOutcomeOutcomeUnknown: "OutcomeUnknown", + } + for got, want := range execution { + if string(got) != want { + t.Errorf("execution outcome = %q, want %q", got, want) + } + } + + delivery := map[TaskDeliveryOutcome]string{ + TaskDeliveryOutcomeNotRequested: "NotRequested", + TaskDeliveryOutcomeVerifiedExact: "VerifiedExact", + TaskDeliveryOutcomeDeliveredSuperseded: "DeliveredSuperseded", + TaskDeliveryOutcomeReadValidated: "ReadValidated", + TaskDeliveryOutcomeNoChange: "NoChange", + TaskDeliveryOutcomeCancelledBeforePublish: "CancelledBeforePublish", + TaskDeliveryOutcomeReadOnlyWorkspaceModified: "ReadOnlyWorkspaceModified", + TaskDeliveryOutcomeDeliveryConflict: "DeliveryConflict", + TaskDeliveryOutcomeCredentialBlocked: "CredentialBlocked", + TaskDeliveryOutcomePublicationOutcomeUnknown: "PublicationOutcomeUnknown", + } + for got, want := range delivery { + if string(got) != want { + t.Errorf("delivery outcome = %q, want %q", got, want) + } + } +} + +func TestTaskWorkspaceHasNoLegacyCredentialOrForkFields(t *testing.T) { + workspace := WorkspaceConfig{ + GitRepo: "https://github.com/example/source.git", + ReadCredentialRef: &WorkspaceCredentialReference{Name: "source-read"}, + PublicationGitRepo: "https://github.com/example/fork.git", + PublicationCredentialRef: &WorkspaceCredentialReference{Name: "fork-write"}, + } + encoded, err := json.Marshal(workspace) + if err != nil { + t.Fatalf("json.Marshal(WorkspaceConfig): %v", err) + } + if strings.Contains(string(encoded), "gitSecretRef") || strings.Contains(string(encoded), "forkRepo") { + t.Fatalf("workspace serialized legacy fields: %s", encoded) + } +} diff --git a/api/v1alpha1/task_session_ref_validation_test.go b/api/v1alpha1/task_session_ref_validation_test.go new file mode 100644 index 000000000..a969a54f7 --- /dev/null +++ b/api/v1alpha1/task_session_ref_validation_test.go @@ -0,0 +1,178 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package v1alpha1 + +import ( + "context" + "maps" + "strings" + "testing" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel" + celconfig "k8s.io/apiserver/pkg/apis/cel" +) + +const ( + agentSessionRefImmutabilityRule = "self.type != 'agent' || (has(self.sessionRef) == has(oldSelf.sessionRef) && (!has(self.sessionRef) || self.sessionRef == oldSelf.sessionRef))" + agentSessionRefImmutabilityMarker = "// +kubebuilder:validation:XValidation:rule=\"" + agentSessionRefImmutabilityRule + "\",message=\"sessionRef is immutable for agent Tasks\"" +) + +func TestAgentSessionRefImmutabilityMarkerAdmission(t *testing.T) { + if source := string(readTaskTypesSource(t)); !strings.Contains(source, agentSessionRefImmutabilityMarker) { + t.Fatalf("TaskSpec is missing the complete agent sessionRef immutability marker: want %q", agentSessionRefImmutabilityMarker) + } + + schema := apiextensions.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensions.JSONSchemaProps{ + "type": {Type: "string"}, + "sessionRef": { + Type: "object", + Properties: map[string]apiextensions.JSONSchemaProps{ + "name": {Type: "string"}, + "create": {Type: "boolean"}, + "append": {Type: "boolean"}, + "maxMessages": {Type: "integer", Format: "int32"}, + "throughMessageId": {Type: "string"}, + "promptIncluded": {Type: "boolean"}, + }, + }, + }, + XValidations: apiextensions.ValidationRules{{ + Rule: agentSessionRefImmutabilityRule, + Message: "sessionRef is immutable for agent Tasks", + }}, + } + structural, err := structuralschema.NewStructural(&schema) + if err != nil { + t.Fatalf("build structural TaskSpec schema: %v", err) + } + validator := cel.NewValidator(structural, false, celconfig.PerCallLimit) + if validator == nil { + t.Fatal("compile agent sessionRef immutability admission rule: validator is nil") + } + + fullSessionRef := map[string]any{ + "name": "session-a", + "create": true, + "append": true, + "maxMessages": int64(50), + "throughMessageId": "message-42", + "promptIncluded": true, + } + oldAgent := taskSpecForSessionRefAdmission("agent", fullSessionRef) + + tests := []struct { + name string + oldSpec map[string]any + newSpec map[string]any + wantErr bool + }{ + { + name: "unchanged complete reference", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", fullSessionRef), + }, + { + name: "add reference", + oldSpec: taskSpecForSessionRefAdmission("agent", nil), + newSpec: taskSpecForSessionRefAdmission("agent", fullSessionRef), + wantErr: true, + }, + { + name: "remove reference", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", nil), + wantErr: true, + }, + { + name: "change name", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", changedSessionRef(fullSessionRef, "name", "session-b")), + wantErr: true, + }, + { + name: "change create", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", changedSessionRef(fullSessionRef, "create", false)), + wantErr: true, + }, + { + name: "change append", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", changedSessionRef(fullSessionRef, "append", false)), + wantErr: true, + }, + { + name: "change max messages", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", changedSessionRef(fullSessionRef, "maxMessages", int64(10))), + wantErr: true, + }, + { + name: "change transcript cutoff", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", changedSessionRef(fullSessionRef, "throughMessageId", "message-41")), + wantErr: true, + }, + { + name: "change prompt included", + oldSpec: oldAgent, + newSpec: taskSpecForSessionRefAdmission("agent", changedSessionRef(fullSessionRef, "promptIncluded", false)), + wantErr: true, + }, + { + name: "container reference remains mutable", + oldSpec: taskSpecForSessionRefAdmission("container", fullSessionRef), + newSpec: taskSpecForSessionRefAdmission("container", changedSessionRef(fullSessionRef, "name", "session-b")), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs, _ := validator.Validate( + context.Background(), + nil, + structural, + tt.newSpec, + tt.oldSpec, + celconfig.RuntimeCELCostBudget, + ) + if tt.wantErr { + if len(errs) == 0 { + t.Fatal("agent sessionRef mutation unexpectedly passed admission") + } + if got := errs.ToAggregate().Error(); !strings.Contains(got, "sessionRef is immutable for agent Tasks") { + t.Fatalf("admission error = %q, want sessionRef immutability message", got) + } + return + } + if len(errs) != 0 { + t.Fatalf("admission unexpectedly rejected update: %v", errs.ToAggregate()) + } + }) + } +} + +func taskSpecForSessionRefAdmission(taskType string, sessionRef map[string]any) map[string]any { + spec := map[string]any{"type": taskType} + if sessionRef != nil { + spec["sessionRef"] = changedSessionRef(sessionRef, "", nil) + } + return spec +} + +func changedSessionRef(sessionRef map[string]any, field string, value any) map[string]any { + changed := make(map[string]any, len(sessionRef)) + maps.Copy(changed, sessionRef) + if field != "" { + changed[field] = value + } + return changed +} diff --git a/api/v1alpha1/task_types.go b/api/v1alpha1/task_types.go index 06fd7f2e9..e5a74b2ac 100644 --- a/api/v1alpha1/task_types.go +++ b/api/v1alpha1/task_types.go @@ -126,7 +126,19 @@ type TaskTransaction struct { // TaskSpec defines the desired state of Task // +kubebuilder:validation:XValidation:rule="has(self.requestedBy) == has(oldSelf.requestedBy) && (!has(self.requestedBy) || self.requestedBy == oldSelf.requestedBy)",message="requestedBy is immutable" // +kubebuilder:validation:XValidation:rule="has(self.transaction) == has(oldSelf.transaction) && (!has(self.transaction) || self.transaction == oldSelf.transaction)",message="transaction is immutable" +// +kubebuilder:validation:XValidation:rule="self.type == oldSelf.type",message="type is immutable" +// +kubebuilder:validation:XValidation:rule="(has(self.workspace) && has(self.workspace.intent) ? self.workspace.intent : (self.type == 'agent' ? 'read' : self.type)) == (has(oldSelf.workspace) && has(oldSelf.workspace.intent) ? oldSelf.workspace.intent : (oldSelf.type == 'agent' ? 'read' : oldSelf.type))",message="effective workspace intent is immutable" +// +kubebuilder:validation:XValidation:rule="self.type != 'agent' || (has(self.prompt) == has(oldSelf.prompt) && (!has(self.prompt) || self.prompt == oldSelf.prompt))",message="agent prompt is immutable" +// +kubebuilder:validation:XValidation:rule="self.type != 'agent' || (has(self.agentRef) == has(oldSelf.agentRef) && (!has(self.agentRef) || self.agentRef == oldSelf.agentRef))",message="agentRef is immutable for agent Tasks" +// +kubebuilder:validation:XValidation:rule="self.type != 'agent' || (has(self.agentRuntime) == has(oldSelf.agentRuntime) && (!has(self.agentRuntime) || self.agentRuntime == oldSelf.agentRuntime))",message="agentRuntime is immutable for agent Tasks" +// +kubebuilder:validation:XValidation:rule="self.type != 'agent' || (has(self.sessionRef) == has(oldSelf.sessionRef) && (!has(self.sessionRef) || self.sessionRef == oldSelf.sessionRef))",message="sessionRef is immutable for agent Tasks" +// +kubebuilder:validation:XValidation:rule="self.type != 'agent' || (has(self.workspace) == has(oldSelf.workspace) && (!has(self.workspace) || self.workspace == oldSelf.workspace))",message="workspace is immutable for agent Tasks" +// +kubebuilder:validation:XValidation:rule="self.type != 'agent' || (has(self.timeout) == has(oldSelf.timeout) && (!has(self.timeout) || self.timeout == oldSelf.timeout))",message="timeout is immutable for agent Tasks" // +kubebuilder:validation:XValidation:rule="!has(self.execution) || !has(self.execution.workspace) || self.execution.workspace.reusePolicy != 'session' || has(self.sessionRef)",message="session workspace reuse requires spec.sessionRef" +// +kubebuilder:validation:XValidation:rule="self.type != 'container' || !has(self.workspace) || !has(self.workspace.expectedRemoteSHA)",message="container Tasks do not support workspace.expectedRemoteSHA" +// +kubebuilder:validation:XValidation:rule="self.type != 'container' || !has(self.workspace) || (!has(self.workspace.createPR) || !self.workspace.createPR)",message="container Tasks do not support workspace.createPR" +// +kubebuilder:validation:XValidation:rule="self.type != 'container' || !has(self.workspace) || (!has(self.workspace.maxChangedFiles) && (!has(self.workspace.allowedPaths) || self.workspace.allowedPaths.size() == 0) && (!has(self.workspace.denyRepositoryControlPaths) || !self.workspace.denyRepositoryControlPaths) && (!has(self.workspace.rejectBinaryFiles) || !self.workspace.rejectBinaryFiles) && (!has(self.workspace.rejectSecretLikeContent) || !self.workspace.rejectSecretLikeContent))",message="container Tasks do not support clean-room workspace publication policies" +// +kubebuilder:validation:XValidation:rule="self.type != 'container' || !has(self.workspace) || !has(self.workspace.pushBranch) || self.workspace.pushBranch.size() == 0 || !has(self.image) || self.image.size() == 0",message="custom-image container Tasks do not support workspace.pushBranch publication" type TaskSpec struct { // Type specifies the task type: "container" or "ai" // +kubebuilder:validation:Required @@ -237,9 +249,9 @@ type TaskSpec struct { // +optional AgentRuntime *AgentRuntimeSpec `json:"agentRuntime,omitempty"` - // Workspace defines repository checkout and push settings for tasks that need - // a git workspace. Agent tasks can continue to use agentRuntime.workspace for - // compatibility; this top-level field is used by container tasks as well. + // Workspace defines the canonical repository workspace, intent, credentials, + // and publication request. Agent Tasks that omit intent are interpreted as + // read by controller logic; an omitted intent preserves existing container behavior. // +optional Workspace *WorkspaceConfig `json:"workspace,omitempty"` @@ -442,10 +454,19 @@ type TaskStatus struct { // +optional ResultRef *ResultReference `json:"resultRef,omitempty"` - // ExecutionOutcome is the immutable outcome recorded when workload execution ends. Workspace - // attachment revocation and cleanup continue independently while the Task is Finalizing. + // Execution reports the durable execution state and terminal outcome for the + // current attempt. Phase remains the compatibility projection. // +optional - ExecutionOutcome *TaskExecutionOutcome `json:"executionOutcome,omitempty"` + Execution *TaskExecutionStatus `json:"execution,omitempty"` + + // Delivery reports trusted workspace validation and publication reconciliation. + // +optional + Delivery *TaskDeliveryStatus `json:"delivery,omitempty"` + + // ExecutionOutcome records the immutable outcome of a non-ACP workload before + // provider-neutral execution-workspace finalization completes. + // +optional + ExecutionOutcome *TaskWorkloadExecutionOutcome `json:"executionOutcome,omitempty"` // ExecutionWorkspace reports the provider-neutral lifecycle state for a // requested execution workspace. Provider-native identifiers and credentials @@ -453,12 +474,6 @@ type TaskStatus struct { // +optional ExecutionWorkspace *ExecutionWorkspaceStatus `json:"executionWorkspace,omitempty"` - // HarnessRuntime records the controller-resolved harness runtime target for an - // in-flight agent turn. It intentionally stores only non-secret routing metadata - // and Secret references, never bearer values. - // +optional - HarnessRuntime *HarnessRuntimeStatus `json:"harnessRuntime,omitempty"` - // WebhookDelivered indicates whether the webhook was successfully called // +optional WebhookDelivered bool `json:"webhookDelivered,omitempty"` @@ -486,43 +501,11 @@ type TaskStatus struct { NextScheduleTime *metav1.Time `json:"nextScheduleTime,omitempty"` } -// HarnessRuntimeStatus records the resolved harness runtime selected by the controller. -type HarnessRuntimeStatus struct { - // RuntimeRefName is the AgentRuntime name for custom runtimeRef turns. Empty means built-in CLI wrapper. - // +optional - RuntimeRefName string `json:"runtimeRefName,omitempty"` - - // RuntimeName is the runtime name advertised by the harness capabilities and sent in turn metadata. - // +optional - RuntimeName string `json:"runtimeName,omitempty"` - - // ContractVersion is the Orka harness contract version used for the turn. - // +optional - ContractVersion string `json:"contractVersion,omitempty"` - - // Endpoint is the non-secret harness base URL selected when the turn started. - // +optional - Endpoint string `json:"endpoint,omitempty"` - - // RuntimeGeneration is the AgentRuntime generation selected when the turn started. - // +optional - RuntimeGeneration int64 `json:"runtimeGeneration,omitempty"` - - // AuthRefName is the Secret name selected when the turn started. - // +optional - AuthRefName string `json:"authRefName,omitempty"` - - // AuthRefField is the Secret data field selected when the turn started. - // +optional - AuthRefField string `json:"authRefField,omitempty"` - - // AuthRefResourceVersion is the auth Secret resourceVersion validated before starting the turn. - // +optional - AuthRefResourceVersion string `json:"authRefResourceVersion,omitempty"` -} - -// TaskExecutionOutcome records the immutable result of workload execution before workspace finalization. -type TaskExecutionOutcome struct { +// TaskWorkloadExecutionOutcome records the immutable result of non-ACP workload +// execution before provider-neutral workspace finalization. ACP agent attempts use +// TaskStatus.Execution, whose stronger fencing and OutcomeUnknown semantics are +// defined in task_runtime_types.go. +type TaskWorkloadExecutionOutcome struct { // Phase is the terminal workload execution phase. // +kubebuilder:validation:Enum=Succeeded;Failed;Cancelled Phase TaskPhase `json:"phase"` @@ -713,27 +696,23 @@ type TaskList struct { } // AgentRuntimeType defines the agent runtime to use -// +kubebuilder:validation:Enum=copilot;claude;codex;opencode +// +kubebuilder:validation:Enum=claude;codex;copilot type AgentRuntimeType string const ( - // AgentRuntimeCopilot uses GitHub Copilot CLI as the agent runtime + // AgentRuntimeCopilot uses GitHub Copilot CLI as the agent runtime. AgentRuntimeCopilot AgentRuntimeType = "copilot" // AgentRuntimeClaude uses Claude Code CLI as the agent runtime AgentRuntimeClaude AgentRuntimeType = "claude" // AgentRuntimeCodex uses OpenAI Codex CLI as the agent runtime AgentRuntimeCodex AgentRuntimeType = "codex" - // AgentRuntimeOpencode uses OpenCode CLI as the agent runtime + // AgentRuntimeOpencode is retained only for rejecting legacy API values in Go tests. AgentRuntimeOpencode AgentRuntimeType = "opencode" ) // AgentRuntimeSpec defines task-level overrides for agent runtime configuration. // Runtime type and credentials come from the referenced Agent CRD. type AgentRuntimeSpec struct { - // Workspace defines the working directory configuration - // +optional - Workspace *WorkspaceConfig `json:"workspace,omitempty"` - // MaxTurns limits the number of agent loop iterations // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=1000 @@ -753,40 +732,131 @@ type AgentRuntimeSpec struct { AllowBash *bool `json:"allowBash,omitempty"` } -// WorkspaceConfig defines workspace setup for agent tasks +// WorkspaceConfig defines repository workspace, validation, and publication intent. +// +kubebuilder:validation:XValidation:rule="!self.createPR || self.intent == 'write'",message="createPR requires write workspace intent" +// +kubebuilder:validation:XValidation:rule="!has(self.gitRepo) || (!self.gitRepo.matches('(?i)^[A-Za-z][A-Za-z0-9+.-]*://[^/]*@') && !self.gitRepo.contains('?') && !self.gitRepo.contains('#'))",message="gitRepo must not contain embedded credentials, query parameters, or fragments" +// +kubebuilder:validation:XValidation:rule="!has(self.publicationGitRepo) || (!self.publicationGitRepo.matches('(?i)^[A-Za-z][A-Za-z0-9+.-]*://[^/]*@') && !self.publicationGitRepo.contains('?') && !self.publicationGitRepo.contains('#'))",message="publicationGitRepo must not contain embedded credentials, query parameters, or fragments" type WorkspaceConfig struct { - // GitRepo is the repository URL to clone + // Intent declares whether the verified workspace must remain unchanged or may + // produce a publication artifact. It is immutable for the lifetime of the Task. + // Agent Tasks that omit intent are interpreted as read by controller logic; + // omitted intent preserves the existing behavior of container Tasks. + // +optional + Intent WorkspaceIntent `json:"intent,omitempty"` + + // GitRepo is the source repository URL cloned by the clean-room workspace boundary. + // Credentials must not be embedded in the URL. + // +kubebuilder:validation:MaxLength=2048 // +optional GitRepo string `json:"gitRepo,omitempty"` - // Branch is the git branch to checkout + // SourceRepository is the optional URL-derived identity for GitRepo. When set, + // it must match the normalized credential-free URL; for GitHub, use provider + // "github" and ID "github.com/owner/repo". + // +optional + SourceRepository *RepositoryIdentity `json:"sourceRepository,omitempty"` + + // Branch is the source branch to check out. + // +kubebuilder:validation:MaxLength=255 // +optional Branch string `json:"branch,omitempty"` - // Ref is a specific git ref (commit SHA, tag) to checkout + // Ref is a specific source git ref, commit SHA, or tag to check out. + // +kubebuilder:validation:MaxLength=512 // +optional Ref string `json:"ref,omitempty"` - // GitSecretRef references a Secret containing git credentials + // ReadCredentialRef references the one-operation clone/read credential Secret. + // The Secret is resolved only by the clean-room workspace boundary. // +optional - GitSecretRef *corev1.LocalObjectReference `json:"gitSecretRef,omitempty"` + ReadCredentialRef *WorkspaceCredentialReference `json:"readCredentialRef,omitempty"` - // SubPath is a subdirectory within the repo to use as workspace root + // PublicationGitRepo is the repository URL whose branch receives an exact CAS publication. + // Credentials must not be embedded in the URL. + // +kubebuilder:validation:MaxLength=2048 // +optional - SubPath string `json:"subPath,omitempty"` + PublicationGitRepo string `json:"publicationGitRepo,omitempty"` + + // PublicationRepository is the optional URL-derived identity for + // PublicationGitRepo. When set, it must match the normalized credential-free + // URL; for GitHub, use provider "github" and ID "github.com/owner/repo". + // +optional + PublicationRepository *RepositoryIdentity `json:"publicationRepository,omitempty"` + + // PublicationReadCredentialRef references the target-repository read + // credential used only for preflight and independent post-push verification. + // +optional + PublicationReadCredentialRef *WorkspaceCredentialReference `json:"publicationReadCredentialRef,omitempty"` - // ForkRepo is the writable fork repository URL for pushing changes + // PublicationCredentialRef references the target-repository write credential + // used only for the exact CAS push. It is never used to clone the source. // +optional - ForkRepo string `json:"forkRepo,omitempty"` + PublicationCredentialRef *WorkspaceCredentialReference `json:"publicationCredentialRef,omitempty"` - // PRBaseBranch is the upstream branch to target for pull requests + // ForgeCredentialRef references the forge API credential used only for pull + // request reconciliation when createPR=true. + // +optional + ForgeCredentialRef *WorkspaceCredentialReference `json:"forgeCredentialRef,omitempty"` + + // SubPath is a subdirectory within the source repository used as workspace root. + // +kubebuilder:validation:MaxLength=1024 + // +optional + SubPath string `json:"subPath,omitempty"` + + // PRBaseBranch is the upstream branch targeted when CreatePR is true. + // +kubebuilder:validation:MaxLength=255 // +optional PRBaseBranch string `json:"prBaseBranch,omitempty"` - // PushBranch is the remote branch name to push changes to after the agent completes. - // When set, FinalizeResult will commit and push changes to this branch. + // PushBranch is the publication branch. For write Tasks the controller derives + // a full-entropy Task- or Session-owned branch when this is omitted. + // +kubebuilder:validation:MaxLength=255 // +optional PushBranch string `json:"pushBranch,omitempty"` + + // ExpectedRemoteSHA requires the publication branch to exist at this exact + // commit before publication. Empty means the branch must be absent. It is + // supported only for agent Tasks using the trusted ACP publisher boundary. + // +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$` + // +optional + ExpectedRemoteSHA string `json:"expectedRemoteSHA,omitempty"` + + // MaxChangedFiles bounds the total changed, deleted, and symlink paths accepted + // from the trusted supervisor before publication. Zero uses the runtime limit. + // It is not supported for container Tasks. + // +kubebuilder:validation:Minimum=1 + // +optional + MaxChangedFiles *int32 `json:"maxChangedFiles,omitempty"` + + // AllowedPaths restricts publishable workspace changes to these path globs or + // directory prefixes ending in /**. Empty allows every otherwise-safe path. + // It is not supported for container Tasks. + // +kubebuilder:validation:MaxItems=256 + // +optional + AllowedPaths []string `json:"allowedPaths,omitempty"` + + // DenyRepositoryControlPaths rejects workflow, RBAC, and chart-secret paths + // before publication even when AllowedPaths is empty or otherwise matches. + // It is not supported for container Tasks. + // +optional + DenyRepositoryControlPaths bool `json:"denyRepositoryControlPaths,omitempty"` + + // RejectBinaryFiles rejects changed file content that is not valid text. It is + // not supported for container Tasks. + // +optional + RejectBinaryFiles bool `json:"rejectBinaryFiles,omitempty"` + + // RejectSecretLikeContent applies Orka's generic secret detector to changed + // paths and file contents before publication. It is not supported for container Tasks. + // +optional + RejectSecretLikeContent bool `json:"rejectSecretLikeContent,omitempty"` + + // CreatePR explicitly requests pull request reconciliation after branch publication. + // Branch push remains the minimum durable delivery when false. It is supported only + // for agent Tasks using the trusted ACP publisher boundary. + // +kubebuilder:default=false + // +optional + CreatePR bool `json:"createPR,omitempty"` } func init() { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2ec71a30b..46acb1d40 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -195,54 +195,12 @@ func (in *AgentRuntime) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRuntimeBearerAuthReference) DeepCopyInto(out *AgentRuntimeBearerAuthReference) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeBearerAuthReference. -func (in *AgentRuntimeBearerAuthReference) DeepCopy() *AgentRuntimeBearerAuthReference { - if in == nil { - return nil - } - out := new(AgentRuntimeBearerAuthReference) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentRuntimeCapabilitiesSpec) DeepCopyInto(out *AgentRuntimeCapabilitiesSpec) { *out = *in - if in.ToolExecutionModes != nil { - in, out := &in.ToolExecutionModes, &out.ToolExecutionModes - *out = make([]AgentRuntimeToolExecutionMode, len(*in)) - copy(*out, *in) - } - if in.BrokeredToolClasses != nil { - in, out := &in.BrokeredToolClasses, &out.BrokeredToolClasses - *out = make([]AgentRuntimeBrokeredToolClass, len(*in)) - copy(*out, *in) - } - if in.SupportsCancel != nil { - in, out := &in.SupportsCancel, &out.SupportsCancel - *out = new(bool) - **out = **in - } - if in.SupportsRuntimeSessions != nil { - in, out := &in.SupportsRuntimeSessions, &out.SupportsRuntimeSessions - *out = new(bool) - **out = **in - } - if in.SupportsContinuation != nil { - in, out := &in.SupportsContinuation, &out.SupportsContinuation - *out = new(bool) - **out = **in - } - if in.SupportsArtifacts != nil { - in, out := &in.SupportsArtifacts, &out.SupportsArtifacts - *out = new(bool) - **out = **in - } + out.Profile = in.Profile + out.Limits = in.Limits + out.WorkspaceGovernance = in.WorkspaceGovernance } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeCapabilitiesSpec. @@ -258,7 +216,8 @@ func (in *AgentRuntimeCapabilitiesSpec) DeepCopy() *AgentRuntimeCapabilitiesSpec // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentRuntimeClientAuth) DeepCopyInto(out *AgentRuntimeClientAuth) { *out = *in - out.BearerAuthRef = in.BearerAuthRef + out.ControllerBearerTokenSecretRef = in.ControllerBearerTokenSecretRef + out.OperationCapabilitySecretRef = in.OperationCapabilitySecretRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeClientAuth. @@ -321,16 +280,8 @@ func (in *AgentRuntimeList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentRuntimeObservedCapabilities) DeepCopyInto(out *AgentRuntimeObservedCapabilities) { *out = *in - if in.ToolExecutionModes != nil { - in, out := &in.ToolExecutionModes, &out.ToolExecutionModes - *out = make([]AgentRuntimeToolExecutionMode, len(*in)) - copy(*out, *in) - } - if in.BrokeredToolClasses != nil { - in, out := &in.BrokeredToolClasses, &out.BrokeredToolClasses - *out = make([]AgentRuntimeBrokeredToolClass, len(*in)) - copy(*out, *in) - } + out.Limits = in.Limits + out.WorkspaceGovernance = in.WorkspaceGovernance } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeObservedCapabilities. @@ -343,6 +294,36 @@ func (in *AgentRuntimeObservedCapabilities) DeepCopy() *AgentRuntimeObservedCapa return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentRuntimeProfileSpec) DeepCopyInto(out *AgentRuntimeProfileSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeProfileSpec. +func (in *AgentRuntimeProfileSpec) DeepCopy() *AgentRuntimeProfileSpec { + if in == nil { + return nil + } + out := new(AgentRuntimeProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentRuntimeProtocolLimits) DeepCopyInto(out *AgentRuntimeProtocolLimits) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeProtocolLimits. +func (in *AgentRuntimeProtocolLimits) DeepCopy() *AgentRuntimeProtocolLimits { + if in == nil { + return nil + } + out := new(AgentRuntimeProtocolLimits) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentRuntimeReference) DeepCopyInto(out *AgentRuntimeReference) { *out = *in @@ -366,7 +347,7 @@ func (in *AgentRuntimeRegistrySpec) DeepCopyInto(out *AgentRuntimeRegistrySpec) if in.Capabilities != nil { in, out := &in.Capabilities, &out.Capabilities *out = new(AgentRuntimeCapabilitiesSpec) - (*in).DeepCopyInto(*out) + **out = **in } } @@ -381,13 +362,23 @@ func (in *AgentRuntimeRegistrySpec) DeepCopy() *AgentRuntimeRegistrySpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRuntimeSpec) DeepCopyInto(out *AgentRuntimeSpec) { +func (in *AgentRuntimeSecretKeyReference) DeepCopyInto(out *AgentRuntimeSecretKeyReference) { *out = *in - if in.Workspace != nil { - in, out := &in.Workspace, &out.Workspace - *out = new(WorkspaceConfig) - (*in).DeepCopyInto(*out) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeSecretKeyReference. +func (in *AgentRuntimeSecretKeyReference) DeepCopy() *AgentRuntimeSecretKeyReference { + if in == nil { + return nil } + out := new(AgentRuntimeSecretKeyReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentRuntimeSpec) DeepCopyInto(out *AgentRuntimeSpec) { + *out = *in if in.MaxTurns != nil { in, out := &in.MaxTurns, &out.MaxTurns *out = new(int32) @@ -426,7 +417,7 @@ func (in *AgentRuntimeStatus) DeepCopyInto(out *AgentRuntimeStatus) { if in.ObservedCapabilities != nil { in, out := &in.ObservedCapabilities, &out.ObservedCapabilities *out = new(AgentRuntimeObservedCapabilities) - (*in).DeepCopyInto(*out) + **out = **in } if in.LastValidated != nil { in, out := &in.LastValidated, &out.LastValidated @@ -451,6 +442,21 @@ func (in *AgentRuntimeStatus) DeepCopy() *AgentRuntimeStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentRuntimeWorkspaceGovernanceCapabilities) DeepCopyInto(out *AgentRuntimeWorkspaceGovernanceCapabilities) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRuntimeWorkspaceGovernanceCapabilities. +func (in *AgentRuntimeWorkspaceGovernanceCapabilities) DeepCopy() *AgentRuntimeWorkspaceGovernanceCapabilities { + if in == nil { + return nil + } + out := new(AgentRuntimeWorkspaceGovernanceCapabilities) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentSpec) DeepCopyInto(out *AgentSpec) { *out = *in @@ -588,470 +594,755 @@ func (in *AzureConfig) DeepCopy() *AzureConfig { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ChildTaskStatus) DeepCopyInto(out *ChildTaskStatus) { +func (in *BranchClaim) DeepCopyInto(out *BranchClaim) { *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChildTaskStatus. -func (in *ChildTaskStatus) DeepCopy() *ChildTaskStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BranchClaim. +func (in *BranchClaim) DeepCopy() *BranchClaim { if in == nil { return nil } - out := new(ChildTaskStatus) + out := new(BranchClaim) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BranchClaim) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConfigMapKeySelector) DeepCopyInto(out *ConfigMapKeySelector) { +func (in *BranchClaimList) DeepCopyInto(out *BranchClaimList) { *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BranchClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapKeySelector. -func (in *ConfigMapKeySelector) DeepCopy() *ConfigMapKeySelector { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BranchClaimList. +func (in *BranchClaimList) DeepCopy() *BranchClaimList { if in == nil { return nil } - out := new(ConfigMapKeySelector) + out := new(BranchClaimList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BranchClaimList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CoordinationConfig) DeepCopyInto(out *CoordinationConfig) { +func (in *BranchClaimSpec) DeepCopyInto(out *BranchClaimSpec) { *out = *in - if in.AllowedAgents != nil { - in, out := &in.AllowedAgents, &out.AllowedAgents - *out = make([]AllowedAgent, len(*in)) - copy(*out, *in) - } - if in.ApprovalRequiredTools != nil { - in, out := &in.ApprovalRequiredTools, &out.ApprovalRequiredTools - *out = make([]string, len(*in)) - copy(*out, *in) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoordinationConfig. -func (in *CoordinationConfig) DeepCopy() *CoordinationConfig { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BranchClaimSpec. +func (in *BranchClaimSpec) DeepCopy() *BranchClaimSpec { if in == nil { return nil } - out := new(CoordinationConfig) + out := new(BranchClaimSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DirectOutboundAccess) DeepCopyInto(out *DirectOutboundAccess) { +func (in *BranchClaimStatus) DeepCopyInto(out *BranchClaimStatus) { *out = *in - in.TokenEndpoint.DeepCopyInto(&out.TokenEndpoint) - in.Subject.DeepCopyInto(&out.Subject) - if in.Actor != nil { - in, out := &in.Actor, &out.Actor - *out = new(OutboundTokenSource) - (*in).DeepCopyInto(*out) - } - if in.Audiences != nil { - in, out := &in.Audiences, &out.Audiences - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Scopes != nil { - in, out := &in.Scopes, &out.Scopes - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.AdditionalParameters != nil { - in, out := &in.AdditionalParameters, &out.AdditionalParameters - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.ClientAuthentication != nil { - in, out := &in.ClientAuthentication, &out.ClientAuthentication - *out = new(OutboundClientAuthentication) - (*in).DeepCopyInto(*out) - } - if in.Output != nil { - in, out := &in.Output, &out.Output - *out = new(OutboundCredentialOutput) - (*in).DeepCopyInto(*out) + if in.LastVerified != nil { + in, out := &in.LastVerified, &out.LastVerified + *out = new(ControlRemoteRefState) + **out = **in } + in.ControlRecordMutationStatus.DeepCopyInto(&out.ControlRecordMutationStatus) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DirectOutboundAccess. -func (in *DirectOutboundAccess) DeepCopy() *DirectOutboundAccess { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BranchClaimStatus. +func (in *BranchClaimStatus) DeepCopy() *BranchClaimStatus { if in == nil { return nil } - out := new(DirectOutboundAccess) + out := new(BranchClaimStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExecutionSpec) DeepCopyInto(out *ExecutionSpec) { +func (in *ChildTaskStatus) DeepCopyInto(out *ChildTaskStatus) { *out = *in - if in.NodeSelector != nil { - in, out := &in.NodeSelector, &out.NodeSelector - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChildTaskStatus. +func (in *ChildTaskStatus) DeepCopy() *ChildTaskStatus { + if in == nil { + return nil } - if in.Tolerations != nil { - in, out := &in.Tolerations, &out.Tolerations - *out = make([]corev1.Toleration, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + out := new(ChildTaskStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMapKeySelector) DeepCopyInto(out *ConfigMapKeySelector) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapKeySelector. +func (in *ConfigMapKeySelector) DeepCopy() *ConfigMapKeySelector { + if in == nil { + return nil } - if in.Affinity != nil { - in, out := &in.Affinity, &out.Affinity - *out = new(corev1.Affinity) - (*in).DeepCopyInto(*out) + out := new(ConfigMapKeySelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlRecordMutationStatus) DeepCopyInto(out *ControlRecordMutationStatus) { + *out = *in + if in.CreatedAt != nil { + in, out := &in.CreatedAt, &out.CreatedAt + *out = (*in).DeepCopy() } - if in.Workspace != nil { - in, out := &in.Workspace, &out.Workspace - *out = new(ExecutionWorkspaceSpec) - (*in).DeepCopyInto(*out) + if in.UpdatedAt != nil { + in, out := &in.UpdatedAt, &out.UpdatedAt + *out = (*in).DeepCopy() } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionSpec. -func (in *ExecutionSpec) DeepCopy() *ExecutionSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlRecordMutationStatus. +func (in *ControlRecordMutationStatus) DeepCopy() *ControlRecordMutationStatus { if in == nil { return nil } - out := new(ExecutionSpec) + out := new(ControlRecordMutationStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExecutionWorkspaceDensityStatus) DeepCopyInto(out *ExecutionWorkspaceDensityStatus) { +func (in *ControlRecordOwner) DeepCopyInto(out *ControlRecordOwner) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceDensityStatus. -func (in *ExecutionWorkspaceDensityStatus) DeepCopy() *ExecutionWorkspaceDensityStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlRecordOwner. +func (in *ControlRecordOwner) DeepCopy() *ControlRecordOwner { if in == nil { return nil } - out := new(ExecutionWorkspaceDensityStatus) + out := new(ControlRecordOwner) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExecutionWorkspaceHibernationSpec) DeepCopyInto(out *ExecutionWorkspaceHibernationSpec) { +func (in *ControlRemoteRefState) DeepCopyInto(out *ControlRemoteRefState) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceHibernationSpec. -func (in *ExecutionWorkspaceHibernationSpec) DeepCopy() *ExecutionWorkspaceHibernationSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlRemoteRefState. +func (in *ControlRemoteRefState) DeepCopy() *ControlRemoteRefState { if in == nil { return nil } - out := new(ExecutionWorkspaceHibernationSpec) + out := new(ControlRemoteRefState) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExecutionWorkspacePlacementStatus) DeepCopyInto(out *ExecutionWorkspacePlacementStatus) { +func (in *ControlVerifiedBranchBaseline) DeepCopyInto(out *ControlVerifiedBranchBaseline) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspacePlacementStatus. -func (in *ExecutionWorkspacePlacementStatus) DeepCopy() *ExecutionWorkspacePlacementStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlVerifiedBranchBaseline. +func (in *ControlVerifiedBranchBaseline) DeepCopy() *ControlVerifiedBranchBaseline { if in == nil { return nil } - out := new(ExecutionWorkspacePlacementStatus) + out := new(ControlVerifiedBranchBaseline) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExecutionWorkspaceSnapshotSpec) DeepCopyInto(out *ExecutionWorkspaceSnapshotSpec) { +func (in *ControllerEpoch) DeepCopyInto(out *ControllerEpoch) { *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceSnapshotSpec. -func (in *ExecutionWorkspaceSnapshotSpec) DeepCopy() *ExecutionWorkspaceSnapshotSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerEpoch. +func (in *ControllerEpoch) DeepCopy() *ControllerEpoch { if in == nil { return nil } - out := new(ExecutionWorkspaceSnapshotSpec) + out := new(ControllerEpoch) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerEpoch) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExecutionWorkspaceSpec) DeepCopyInto(out *ExecutionWorkspaceSpec) { +func (in *ControllerEpochList) DeepCopyInto(out *ControllerEpochList) { *out = *in - if in.ClassRef != nil { - in, out := &in.ClassRef, &out.ClassRef - *out = new(WorkspaceClassReference) - **out = **in - } - if in.TemplateRef != nil { - in, out := &in.TemplateRef, &out.TemplateRef - *out = new(WorkspaceTemplateReference) - **out = **in - } - if in.PoolRef != nil { - in, out := &in.PoolRef, &out.PoolRef - *out = new(SubstrateActorPoolReference) - **out = **in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ControllerEpoch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - if in.Snapshot != nil { - in, out := &in.Snapshot, &out.Snapshot - *out = new(ExecutionWorkspaceSnapshotSpec) - **out = **in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerEpochList. +func (in *ControllerEpochList) DeepCopy() *ControllerEpochList { + if in == nil { + return nil } - if in.Hibernation != nil { - in, out := &in.Hibernation, &out.Hibernation - *out = new(ExecutionWorkspaceHibernationSpec) - **out = **in + out := new(ControllerEpochList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerEpochList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c } + return nil } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceSpec. -func (in *ExecutionWorkspaceSpec) DeepCopy() *ExecutionWorkspaceSpec { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerEpochSpec) DeepCopyInto(out *ControllerEpochSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerEpochSpec. +func (in *ControllerEpochSpec) DeepCopy() *ControllerEpochSpec { if in == nil { return nil } - out := new(ExecutionWorkspaceSpec) + out := new(ControllerEpochSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExecutionWorkspaceStatus) DeepCopyInto(out *ExecutionWorkspaceStatus) { +func (in *ControllerEpochStatus) DeepCopyInto(out *ControllerEpochStatus) { *out = *in - if in.ClassRef != nil { - in, out := &in.ClassRef, &out.ClassRef - *out = new(WorkspaceClassReference) - **out = **in - } - if in.WorkspaceRef != nil { - in, out := &in.WorkspaceRef, &out.WorkspaceRef - *out = new(WorkspaceObjectReference) - **out = **in - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.TemplateRef != nil { - in, out := &in.TemplateRef, &out.TemplateRef - *out = new(WorkspaceTemplateReference) - **out = **in - } - if in.Placement != nil { - in, out := &in.Placement, &out.Placement - *out = new(ExecutionWorkspacePlacementStatus) - **out = **in - } - if in.Density != nil { - in, out := &in.Density, &out.Density - *out = new(ExecutionWorkspaceDensityStatus) - **out = **in - } - if in.ResumeLatency != nil { - in, out := &in.ResumeLatency, &out.ResumeLatency - *out = new(v1.Duration) - **out = **in + if in.AcquiredAt != nil { + in, out := &in.AcquiredAt, &out.AcquiredAt + *out = (*in).DeepCopy() } - if in.LastUpdateTime != nil { - in, out := &in.LastUpdateTime, &out.LastUpdateTime + if in.UpdatedAt != nil { + in, out := &in.UpdatedAt, &out.UpdatedAt *out = (*in).DeepCopy() } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceStatus. -func (in *ExecutionWorkspaceStatus) DeepCopy() *ExecutionWorkspaceStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerEpochStatus. +func (in *ControllerEpochStatus) DeepCopy() *ControllerEpochStatus { if in == nil { return nil } - out := new(ExecutionWorkspaceStatus) + out := new(ControllerEpochStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FindingCountsStatus) DeepCopyInto(out *FindingCountsStatus) { +func (in *CoordinationConfig) DeepCopyInto(out *CoordinationConfig) { *out = *in + if in.AllowedAgents != nil { + in, out := &in.AllowedAgents, &out.AllowedAgents + *out = make([]AllowedAgent, len(*in)) + copy(*out, *in) + } + if in.ApprovalRequiredTools != nil { + in, out := &in.ApprovalRequiredTools, &out.ApprovalRequiredTools + *out = make([]string, len(*in)) + copy(*out, *in) + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FindingCountsStatus. -func (in *FindingCountsStatus) DeepCopy() *FindingCountsStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoordinationConfig. +func (in *CoordinationConfig) DeepCopy() *CoordinationConfig { if in == nil { return nil } - out := new(FindingCountsStatus) + out := new(CoordinationConfig) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GatewayOutboundAccess) DeepCopyInto(out *GatewayOutboundAccess) { +func (in *DirectOutboundAccess) DeepCopyInto(out *DirectOutboundAccess) { *out = *in - out.ServiceRef = in.ServiceRef - if in.TLS != nil { - in, out := &in.TLS, &out.TLS - *out = new(OutboundTLSConfig) + in.TokenEndpoint.DeepCopyInto(&out.TokenEndpoint) + in.Subject.DeepCopyInto(&out.Subject) + if in.Actor != nil { + in, out := &in.Actor, &out.Actor + *out = new(OutboundTokenSource) + (*in).DeepCopyInto(*out) + } + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AdditionalParameters != nil { + in, out := &in.AdditionalParameters, &out.AdditionalParameters + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ClientAuthentication != nil { + in, out := &in.ClientAuthentication, &out.ClientAuthentication + *out = new(OutboundClientAuthentication) + (*in).DeepCopyInto(*out) + } + if in.Output != nil { + in, out := &in.Output, &out.Output + *out = new(OutboundCredentialOutput) (*in).DeepCopyInto(*out) } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayOutboundAccess. -func (in *GatewayOutboundAccess) DeepCopy() *GatewayOutboundAccess { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DirectOutboundAccess. +func (in *DirectOutboundAccess) DeepCopy() *DirectOutboundAccess { if in == nil { return nil } - out := new(GatewayOutboundAccess) + out := new(DirectOutboundAccess) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HTTPExecution) DeepCopyInto(out *HTTPExecution) { +func (in *ExecutionSpec) DeepCopyInto(out *ExecutionSpec) { *out = *in - if in.Headers != nil { - in, out := &in.Headers, &out.Headers + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } - if in.Timeout != nil { - in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) - **out = **in + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - if in.AuthSecretRef != nil { - in, out := &in.AuthSecretRef, &out.AuthSecretRef - *out = new(SecretKeySelector) - **out = **in + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) } - if in.OutboundAccessPolicyRef != nil { - in, out := &in.OutboundAccessPolicyRef, &out.OutboundAccessPolicyRef - *out = new(LocalObjectReference) - **out = **in + if in.Workspace != nil { + in, out := &in.Workspace, &out.Workspace + *out = new(ExecutionWorkspaceSpec) + (*in).DeepCopyInto(*out) } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPExecution. -func (in *HTTPExecution) DeepCopy() *HTTPExecution { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionSpec. +func (in *ExecutionSpec) DeepCopy() *ExecutionSpec { if in == nil { return nil } - out := new(HTTPExecution) + out := new(ExecutionSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HarnessRuntimeStatus) DeepCopyInto(out *HarnessRuntimeStatus) { +func (in *ExecutionWorkspaceDensityStatus) DeepCopyInto(out *ExecutionWorkspaceDensityStatus) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HarnessRuntimeStatus. -func (in *HarnessRuntimeStatus) DeepCopy() *HarnessRuntimeStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceDensityStatus. +func (in *ExecutionWorkspaceDensityStatus) DeepCopy() *ExecutionWorkspaceDensityStatus { if in == nil { return nil } - out := new(HarnessRuntimeStatus) + out := new(ExecutionWorkspaceDensityStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) { +func (in *ExecutionWorkspaceHibernationSpec) DeepCopyInto(out *ExecutionWorkspaceHibernationSpec) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectReference. -func (in *LocalObjectReference) DeepCopy() *LocalObjectReference { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceHibernationSpec. +func (in *ExecutionWorkspaceHibernationSpec) DeepCopy() *ExecutionWorkspaceHibernationSpec { if in == nil { return nil } - out := new(LocalObjectReference) + out := new(ExecutionWorkspaceHibernationSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MCPToolServer) DeepCopyInto(out *MCPToolServer) { +func (in *ExecutionWorkspacePlacementStatus) DeepCopyInto(out *ExecutionWorkspacePlacementStatus) { *out = *in - if in.Workspace != nil { - in, out := &in.Workspace, &out.Workspace - *out = new(MCPWorkspace) - **out = **in - } - if in.SubstrateActor != nil { - in, out := &in.SubstrateActor, &out.SubstrateActor - *out = new(SubstrateMCPActor) - (*in).DeepCopyInto(*out) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MCPToolServer. -func (in *MCPToolServer) DeepCopy() *MCPToolServer { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspacePlacementStatus. +func (in *ExecutionWorkspacePlacementStatus) DeepCopy() *ExecutionWorkspacePlacementStatus { if in == nil { return nil } - out := new(MCPToolServer) + out := new(ExecutionWorkspacePlacementStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MCPWorkspace) DeepCopyInto(out *MCPWorkspace) { +func (in *ExecutionWorkspaceSnapshotSpec) DeepCopyInto(out *ExecutionWorkspaceSnapshotSpec) { *out = *in - out.ClassRef = in.ClassRef } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MCPWorkspace. -func (in *MCPWorkspace) DeepCopy() *MCPWorkspace { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceSnapshotSpec. +func (in *ExecutionWorkspaceSnapshotSpec) DeepCopy() *ExecutionWorkspaceSnapshotSpec { if in == nil { return nil } - out := new(MCPWorkspace) + out := new(ExecutionWorkspaceSnapshotSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ModelConfig) DeepCopyInto(out *ModelConfig) { +func (in *ExecutionWorkspaceSpec) DeepCopyInto(out *ExecutionWorkspaceSpec) { *out = *in - if in.Temperature != nil { - in, out := &in.Temperature, &out.Temperature - *out = new(float64) + if in.ClassRef != nil { + in, out := &in.ClassRef, &out.ClassRef + *out = new(WorkspaceClassReference) **out = **in } - if in.MaxTokens != nil { - in, out := &in.MaxTokens, &out.MaxTokens - *out = new(int32) + if in.TemplateRef != nil { + in, out := &in.TemplateRef, &out.TemplateRef + *out = new(WorkspaceTemplateReference) **out = **in } - if in.Fallbacks != nil { - in, out := &in.Fallbacks, &out.Fallbacks - *out = make([]ModelFallback, len(*in)) - copy(*out, *in) + if in.PoolRef != nil { + in, out := &in.PoolRef, &out.PoolRef + *out = new(SubstrateActorPoolReference) + **out = **in + } + if in.Snapshot != nil { + in, out := &in.Snapshot, &out.Snapshot + *out = new(ExecutionWorkspaceSnapshotSpec) + **out = **in + } + if in.Hibernation != nil { + in, out := &in.Hibernation, &out.Hibernation + *out = new(ExecutionWorkspaceHibernationSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceSpec. +func (in *ExecutionWorkspaceSpec) DeepCopy() *ExecutionWorkspaceSpec { + if in == nil { + return nil + } + out := new(ExecutionWorkspaceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExecutionWorkspaceStatus) DeepCopyInto(out *ExecutionWorkspaceStatus) { + *out = *in + if in.ClassRef != nil { + in, out := &in.ClassRef, &out.ClassRef + *out = new(WorkspaceClassReference) + **out = **in + } + if in.WorkspaceRef != nil { + in, out := &in.WorkspaceRef, &out.WorkspaceRef + *out = new(WorkspaceObjectReference) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TemplateRef != nil { + in, out := &in.TemplateRef, &out.TemplateRef + *out = new(WorkspaceTemplateReference) + **out = **in + } + if in.Placement != nil { + in, out := &in.Placement, &out.Placement + *out = new(ExecutionWorkspacePlacementStatus) + **out = **in + } + if in.Density != nil { + in, out := &in.Density, &out.Density + *out = new(ExecutionWorkspaceDensityStatus) + **out = **in + } + if in.ResumeLatency != nil { + in, out := &in.ResumeLatency, &out.ResumeLatency + *out = new(v1.Duration) + **out = **in + } + if in.LastUpdateTime != nil { + in, out := &in.LastUpdateTime, &out.LastUpdateTime + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionWorkspaceStatus. +func (in *ExecutionWorkspaceStatus) DeepCopy() *ExecutionWorkspaceStatus { + if in == nil { + return nil + } + out := new(ExecutionWorkspaceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalEffectSpec) DeepCopyInto(out *ExternalEffectSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalEffectSpec. +func (in *ExternalEffectSpec) DeepCopy() *ExternalEffectSpec { + if in == nil { + return nil + } + out := new(ExternalEffectSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalEffectStatus) DeepCopyInto(out *ExternalEffectStatus) { + *out = *in + if in.Response != nil { + in, out := &in.Response, &out.Response + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } + if in.LeaseExpiresAt != nil { + in, out := &in.LeaseExpiresAt, &out.LeaseExpiresAt + *out = (*in).DeepCopy() + } + in.ControlRecordMutationStatus.DeepCopyInto(&out.ControlRecordMutationStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalEffectStatus. +func (in *ExternalEffectStatus) DeepCopy() *ExternalEffectStatus { + if in == nil { + return nil + } + out := new(ExternalEffectStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FindingCountsStatus) DeepCopyInto(out *FindingCountsStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FindingCountsStatus. +func (in *FindingCountsStatus) DeepCopy() *FindingCountsStatus { + if in == nil { + return nil + } + out := new(FindingCountsStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayOutboundAccess) DeepCopyInto(out *GatewayOutboundAccess) { + *out = *in + out.ServiceRef = in.ServiceRef + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(OutboundTLSConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayOutboundAccess. +func (in *GatewayOutboundAccess) DeepCopy() *GatewayOutboundAccess { + if in == nil { + return nil + } + out := new(GatewayOutboundAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPExecution) DeepCopyInto(out *HTTPExecution) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } + if in.AuthSecretRef != nil { + in, out := &in.AuthSecretRef, &out.AuthSecretRef + *out = new(SecretKeySelector) + **out = **in + } + if in.OutboundAccessPolicyRef != nil { + in, out := &in.OutboundAccessPolicyRef, &out.OutboundAccessPolicyRef + *out = new(LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPExecution. +func (in *HTTPExecution) DeepCopy() *HTTPExecution { + if in == nil { + return nil + } + out := new(HTTPExecution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectReference. +func (in *LocalObjectReference) DeepCopy() *LocalObjectReference { + if in == nil { + return nil + } + out := new(LocalObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MCPToolServer) DeepCopyInto(out *MCPToolServer) { + *out = *in + if in.Workspace != nil { + in, out := &in.Workspace, &out.Workspace + *out = new(MCPWorkspace) + **out = **in + } + if in.SubstrateActor != nil { + in, out := &in.SubstrateActor, &out.SubstrateActor + *out = new(SubstrateMCPActor) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MCPToolServer. +func (in *MCPToolServer) DeepCopy() *MCPToolServer { + if in == nil { + return nil + } + out := new(MCPToolServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MCPWorkspace) DeepCopyInto(out *MCPWorkspace) { + *out = *in + out.ClassRef = in.ClassRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MCPWorkspace. +func (in *MCPWorkspace) DeepCopy() *MCPWorkspace { + if in == nil { + return nil + } + out := new(MCPWorkspace) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ModelConfig) DeepCopyInto(out *ModelConfig) { + *out = *in + if in.Temperature != nil { + in, out := &in.Temperature, &out.Temperature + *out = new(float64) + **out = **in + } + if in.MaxTokens != nil { + in, out := &in.MaxTokens, &out.MaxTokens + *out = new(int32) + **out = **in + } + if in.Fallbacks != nil { + in, out := &in.Fallbacks, &out.Fallbacks + *out = make([]ModelFallback, len(*in)) + copy(*out, *in) } } @@ -1372,16 +1663,142 @@ func (in *PolicyConfigMapKeyRef) DeepCopy() *PolicyConfigMapKeyRef { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PriorTaskReference) DeepCopyInto(out *PriorTaskReference) { +func (in *PreparedPublicationControlReceipt) DeepCopyInto(out *PreparedPublicationControlReceipt) { *out = *in + in.PreparedAt.DeepCopyInto(&out.PreparedAt) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PriorTaskReference. -func (in *PriorTaskReference) DeepCopy() *PriorTaskReference { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PreparedPublicationControlReceipt. +func (in *PreparedPublicationControlReceipt) DeepCopy() *PreparedPublicationControlReceipt { if in == nil { return nil } - out := new(PriorTaskReference) + out := new(PreparedPublicationControlReceipt) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PriorTaskReference) DeepCopyInto(out *PriorTaskReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PriorTaskReference. +func (in *PriorTaskReference) DeepCopy() *PriorTaskReference { + if in == nil { + return nil + } + out := new(PriorTaskReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromptAttempt) DeepCopyInto(out *PromptAttempt) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromptAttempt. +func (in *PromptAttempt) DeepCopy() *PromptAttempt { + if in == nil { + return nil + } + out := new(PromptAttempt) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PromptAttempt) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromptAttemptList) DeepCopyInto(out *PromptAttemptList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PromptAttempt, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromptAttemptList. +func (in *PromptAttemptList) DeepCopy() *PromptAttemptList { + if in == nil { + return nil + } + out := new(PromptAttemptList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PromptAttemptList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromptAttemptSpec) DeepCopyInto(out *PromptAttemptSpec) { + *out = *in + if in.CredentialBindings != nil { + in, out := &in.CredentialBindings, &out.CredentialBindings + *out = make([]PromptCredentialBinding, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromptAttemptSpec. +func (in *PromptAttemptSpec) DeepCopy() *PromptAttemptSpec { + if in == nil { + return nil + } + out := new(PromptAttemptSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromptAttemptStatus) DeepCopyInto(out *PromptAttemptStatus) { + *out = *in + in.ControlRecordMutationStatus.DeepCopyInto(&out.ControlRecordMutationStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromptAttemptStatus. +func (in *PromptAttemptStatus) DeepCopy() *PromptAttemptStatus { + if in == nil { + return nil + } + out := new(PromptAttemptStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromptCredentialBinding) DeepCopyInto(out *PromptCredentialBinding) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromptCredentialBinding. +func (in *PromptCredentialBinding) DeepCopy() *PromptCredentialBinding { + if in == nil { + return nil + } + out := new(PromptCredentialBinding) in.DeepCopyInto(out) return out } @@ -1572,6 +1989,188 @@ func (in *ProviderStatus) DeepCopy() *ProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Publication) DeepCopyInto(out *Publication) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Publication. +func (in *Publication) DeepCopy() *Publication { + if in == nil { + return nil + } + out := new(Publication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Publication) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicationList) DeepCopyInto(out *PublicationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Publication, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicationList. +func (in *PublicationList) DeepCopy() *PublicationList { + if in == nil { + return nil + } + out := new(PublicationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PublicationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicationPullRequestIntent) DeepCopyInto(out *PublicationPullRequestIntent) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicationPullRequestIntent. +func (in *PublicationPullRequestIntent) DeepCopy() *PublicationPullRequestIntent { + if in == nil { + return nil + } + out := new(PublicationPullRequestIntent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicationSpec) DeepCopyInto(out *PublicationSpec) { + *out = *in + out.Baseline = in.Baseline + in.CommitTimestamp.DeepCopyInto(&out.CommitTimestamp) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicationSpec. +func (in *PublicationSpec) DeepCopy() *PublicationSpec { + if in == nil { + return nil + } + out := new(PublicationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicationStatus) DeepCopyInto(out *PublicationStatus) { + *out = *in + if in.PRIntent != nil { + in, out := &in.PRIntent, &out.PRIntent + *out = new(PublicationPullRequestIntent) + **out = **in + } + if in.PreparedReceipt != nil { + in, out := &in.PreparedReceipt, &out.PreparedReceipt + *out = new(PreparedPublicationControlReceipt) + (*in).DeepCopyInto(*out) + } + if in.PublishReceipt != nil { + in, out := &in.PublishReceipt, &out.PublishReceipt + *out = new(PublishOperationControlReceipt) + (*in).DeepCopyInto(*out) + } + if in.VerificationReceipt != nil { + in, out := &in.VerificationReceipt, &out.VerificationReceipt + *out = new(PublicationVerificationControlReceipt) + (*in).DeepCopyInto(*out) + } + if in.PullRequestReceipt != nil { + in, out := &in.PullRequestReceipt, &out.PullRequestReceipt + *out = new(PullRequestOperationControlReceipt) + (*in).DeepCopyInto(*out) + } + in.ControlRecordMutationStatus.DeepCopyInto(&out.ControlRecordMutationStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicationStatus. +func (in *PublicationStatus) DeepCopy() *PublicationStatus { + if in == nil { + return nil + } + out := new(PublicationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicationVerificationControlReceipt) DeepCopyInto(out *PublicationVerificationControlReceipt) { + *out = *in + out.ObservedRemote = in.ObservedRemote + in.VerifiedAt.DeepCopyInto(&out.VerifiedAt) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicationVerificationControlReceipt. +func (in *PublicationVerificationControlReceipt) DeepCopy() *PublicationVerificationControlReceipt { + if in == nil { + return nil + } + out := new(PublicationVerificationControlReceipt) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublishOperationControlReceipt) DeepCopyInto(out *PublishOperationControlReceipt) { + *out = *in + out.RemoteBefore = in.RemoteBefore + in.PublishedAt.DeepCopyInto(&out.PublishedAt) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishOperationControlReceipt. +func (in *PublishOperationControlReceipt) DeepCopy() *PublishOperationControlReceipt { + if in == nil { + return nil + } + out := new(PublishOperationControlReceipt) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PullRequestOperationControlReceipt) DeepCopyInto(out *PullRequestOperationControlReceipt) { + *out = *in + in.ReconciledAt.DeepCopyInto(&out.ReconciledAt) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PullRequestOperationControlReceipt. +func (in *PullRequestOperationControlReceipt) DeepCopy() *PullRequestOperationControlReceipt { + if in == nil { + return nil + } + out := new(PullRequestOperationControlReceipt) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimitConfig) DeepCopyInto(out *RateLimitConfig) { *out = *in @@ -1597,6 +2196,21 @@ func (in *RateLimitConfig) DeepCopy() *RateLimitConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryIdentity) DeepCopyInto(out *RepositoryIdentity) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryIdentity. +func (in *RepositoryIdentity) DeepCopy() *RepositoryIdentity { + if in == nil { + return nil + } + out := new(RepositoryIdentity) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RepositoryMonitor) DeepCopyInto(out *RepositoryMonitor) { *out = *in @@ -1935,372 +2549,818 @@ func (in *RepositoryMonitorList) DeepCopyInto(out *RepositoryMonitorList) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorList. -func (in *RepositoryMonitorList) DeepCopy() *RepositoryMonitorList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorList. +func (in *RepositoryMonitorList) DeepCopy() *RepositoryMonitorList { + if in == nil { + return nil + } + out := new(RepositoryMonitorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RepositoryMonitorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorOptInLabels) DeepCopyInto(out *RepositoryMonitorOptInLabels) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorOptInLabels. +func (in *RepositoryMonitorOptInLabels) DeepCopy() *RepositoryMonitorOptInLabels { + if in == nil { + return nil + } + out := new(RepositoryMonitorOptInLabels) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorPolicySpec) DeepCopyInto(out *RepositoryMonitorPolicySpec) { + *out = *in + if in.ProtectedLabels != nil { + in, out := &in.ProtectedLabels, &out.ProtectedLabels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PauseLabels != nil { + in, out := &in.PauseLabels, &out.PauseLabels + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.OptInLabels = in.OptInLabels + out.AdvisoryLabels = in.AdvisoryLabels + if in.AllowedRepositoryPermissions != nil { + in, out := &in.AllowedRepositoryPermissions, &out.AllowedRepositoryPermissions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorPolicySpec. +func (in *RepositoryMonitorPolicySpec) DeepCopy() *RepositoryMonitorPolicySpec { + if in == nil { + return nil + } + out := new(RepositoryMonitorPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorPullRequestCommandLabels) DeepCopyInto(out *RepositoryMonitorPullRequestCommandLabels) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorPullRequestCommandLabels. +func (in *RepositoryMonitorPullRequestCommandLabels) DeepCopy() *RepositoryMonitorPullRequestCommandLabels { + if in == nil { + return nil + } + out := new(RepositoryMonitorPullRequestCommandLabels) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorPullRequestTarget) DeepCopyInto(out *RepositoryMonitorPullRequestTarget) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.MaxPerRun != nil { + in, out := &in.MaxPerRun, &out.MaxPerRun + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorPullRequestTarget. +func (in *RepositoryMonitorPullRequestTarget) DeepCopy() *RepositoryMonitorPullRequestTarget { + if in == nil { + return nil + } + out := new(RepositoryMonitorPullRequestTarget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorRepairSpec) DeepCopyInto(out *RepositoryMonitorRepairSpec) { + *out = *in + if in.RequireMaintainerOptIn != nil { + in, out := &in.RequireMaintainerOptIn, &out.RequireMaintainerOptIn + *out = new(bool) + **out = **in + } + if in.MaxRepairsPerPR != nil { + in, out := &in.MaxRepairsPerPR, &out.MaxRepairsPerPR + *out = new(int32) + **out = **in + } + if in.MaxRepairsPerHead != nil { + in, out := &in.MaxRepairsPerHead, &out.MaxRepairsPerHead + *out = new(int32) + **out = **in + } + if in.MaxValidationRetries != nil { + in, out := &in.MaxValidationRetries, &out.MaxValidationRetries + *out = new(int32) + **out = **in + } + if in.MaxReviewFixRetries != nil { + in, out := &in.MaxReviewFixRetries, &out.MaxReviewFixRetries + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorRepairSpec. +func (in *RepositoryMonitorRepairSpec) DeepCopy() *RepositoryMonitorRepairSpec { + if in == nil { + return nil + } + out := new(RepositoryMonitorRepairSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorReviewPublishInlineSpec) DeepCopyInto(out *RepositoryMonitorReviewPublishInlineSpec) { + *out = *in + if in.MaxComments != nil { + in, out := &in.MaxComments, &out.MaxComments + *out = new(int32) + **out = **in + } + if in.OnlyChangedLines != nil { + in, out := &in.OnlyChangedLines, &out.OnlyChangedLines + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorReviewPublishInlineSpec. +func (in *RepositoryMonitorReviewPublishInlineSpec) DeepCopy() *RepositoryMonitorReviewPublishInlineSpec { + if in == nil { + return nil + } + out := new(RepositoryMonitorReviewPublishInlineSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorReviewPublishSpec) DeepCopyInto(out *RepositoryMonitorReviewPublishSpec) { + *out = *in + if in.PostPassed != nil { + in, out := &in.PostPassed, &out.PostPassed + *out = new(bool) + **out = **in + } + if in.PostNeedsChanges != nil { + in, out := &in.PostNeedsChanges, &out.PostNeedsChanges + *out = new(bool) + **out = **in + } + if in.PostNeedsHuman != nil { + in, out := &in.PostNeedsHuman, &out.PostNeedsHuman + *out = new(bool) + **out = **in + } + in.Inline.DeepCopyInto(&out.Inline) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorReviewPublishSpec. +func (in *RepositoryMonitorReviewPublishSpec) DeepCopy() *RepositoryMonitorReviewPublishSpec { + if in == nil { + return nil + } + out := new(RepositoryMonitorReviewPublishSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorReviewSpec) DeepCopyInto(out *RepositoryMonitorReviewSpec) { + *out = *in + if in.StaleReviewTTL != nil { + in, out := &in.StaleReviewTTL, &out.StaleReviewTTL + *out = new(v1.Duration) + **out = **in + } + in.Publish.DeepCopyInto(&out.Publish) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorReviewSpec. +func (in *RepositoryMonitorReviewSpec) DeepCopy() *RepositoryMonitorReviewSpec { + if in == nil { + return nil + } + out := new(RepositoryMonitorReviewSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorSpec) DeepCopyInto(out *RepositoryMonitorSpec) { + *out = *in + if in.GitSecretRef != nil { + in, out := &in.GitSecretRef, &out.GitSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.TimeZone != nil { + in, out := &in.TimeZone, &out.TimeZone + *out = new(string) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(bool) + **out = **in + } + in.Targets.DeepCopyInto(&out.Targets) + out.Triggers = in.Triggers + in.Agents.DeepCopyInto(&out.Agents) + in.IssueWorkflow.DeepCopyInto(&out.IssueWorkflow) + in.Review.DeepCopyInto(&out.Review) + in.Repair.DeepCopyInto(&out.Repair) + in.Automerge.DeepCopyInto(&out.Automerge) + in.Policy.DeepCopyInto(&out.Policy) + in.Validation.DeepCopyInto(&out.Validation) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorSpec. +func (in *RepositoryMonitorSpec) DeepCopy() *RepositoryMonitorSpec { + if in == nil { + return nil + } + out := new(RepositoryMonitorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorStatus) DeepCopyInto(out *RepositoryMonitorStatus) { + *out = *in + if in.LastRunTime != nil { + in, out := &in.LastRunTime, &out.LastRunTime + *out = (*in).DeepCopy() + } + if in.LastSuccessfulRunTime != nil { + in, out := &in.LastSuccessfulRunTime, &out.LastSuccessfulRunTime + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorStatus. +func (in *RepositoryMonitorStatus) DeepCopy() *RepositoryMonitorStatus { + if in == nil { + return nil + } + out := new(RepositoryMonitorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorTargets) DeepCopyInto(out *RepositoryMonitorTargets) { + *out = *in + in.PullRequests.DeepCopyInto(&out.PullRequests) + in.Issues.DeepCopyInto(&out.Issues) + in.Commits.DeepCopyInto(&out.Commits) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorTargets. +func (in *RepositoryMonitorTargets) DeepCopy() *RepositoryMonitorTargets { + if in == nil { + return nil + } + out := new(RepositoryMonitorTargets) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorTriggers) DeepCopyInto(out *RepositoryMonitorTriggers) { + *out = *in + out.GitHub = in.GitHub +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorTriggers. +func (in *RepositoryMonitorTriggers) DeepCopy() *RepositoryMonitorTriggers { + if in == nil { + return nil + } + out := new(RepositoryMonitorTriggers) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryMonitorValidationSpec) DeepCopyInto(out *RepositoryMonitorValidationSpec) { + *out = *in + if in.Commands != nil { + in, out := &in.Commands, &out.Commands + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorValidationSpec. +func (in *RepositoryMonitorValidationSpec) DeepCopy() *RepositoryMonitorValidationSpec { + if in == nil { + return nil + } + out := new(RepositoryMonitorValidationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryScan) DeepCopyInto(out *RepositoryScan) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScan. +func (in *RepositoryScan) DeepCopy() *RepositoryScan { + if in == nil { + return nil + } + out := new(RepositoryScan) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RepositoryScan) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryScanList) DeepCopyInto(out *RepositoryScanList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RepositoryScan, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScanList. +func (in *RepositoryScanList) DeepCopy() *RepositoryScanList { + if in == nil { + return nil + } + out := new(RepositoryScanList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RepositoryScanList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryScanSpec) DeepCopyInto(out *RepositoryScanSpec) { + *out = *in + if in.GitSecretRef != nil { + in, out := &in.GitSecretRef, &out.GitSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.TimeZone != nil { + in, out := &in.TimeZone, &out.TimeZone + *out = new(string) + **out = **in + } + if in.HistoryDays != nil { + in, out := &in.HistoryDays, &out.HistoryDays + *out = new(int32) + **out = **in + } + if in.ValidationMaxFindingsPerRun != nil { + in, out := &in.ValidationMaxFindingsPerRun, &out.ValidationMaxFindingsPerRun + *out = new(int32) + **out = **in + } + if in.CustomScanInstructionsRef != nil { + in, out := &in.CustomScanInstructionsRef, &out.CustomScanInstructionsRef + *out = new(PolicyConfigMapKeyRef) + **out = **in + } + if in.FalsePositivePolicyRef != nil { + in, out := &in.FalsePositivePolicyRef, &out.FalsePositivePolicyRef + *out = new(PolicyConfigMapKeyRef) + **out = **in + } + out.AnalysisAgentRef = in.AnalysisAgentRef + if in.PatchAgentRef != nil { + in, out := &in.PatchAgentRef, &out.PatchAgentRef + *out = new(AgentReference) + **out = **in + } + if in.MaxFindingsPerRun != nil { + in, out := &in.MaxFindingsPerRun, &out.MaxFindingsPerRun + *out = new(int32) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScanSpec. +func (in *RepositoryScanSpec) DeepCopy() *RepositoryScanSpec { if in == nil { return nil } - out := new(RepositoryMonitorList) + out := new(RepositoryScanSpec) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RepositoryMonitorList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorOptInLabels) DeepCopyInto(out *RepositoryMonitorOptInLabels) { +func (in *RepositoryScanStatus) DeepCopyInto(out *RepositoryScanStatus) { *out = *in + if in.LastScanAt != nil { + in, out := &in.LastScanAt, &out.LastScanAt + *out = (*in).DeepCopy() + } + if in.LastSuccessfulScanAt != nil { + in, out := &in.LastSuccessfulScanAt, &out.LastSuccessfulScanAt + *out = (*in).DeepCopy() + } + out.FindingCounts = in.FindingCounts + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorOptInLabels. -func (in *RepositoryMonitorOptInLabels) DeepCopy() *RepositoryMonitorOptInLabels { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScanStatus. +func (in *RepositoryScanStatus) DeepCopy() *RepositoryScanStatus { if in == nil { return nil } - out := new(RepositoryMonitorOptInLabels) + out := new(RepositoryScanStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorPolicySpec) DeepCopyInto(out *RepositoryMonitorPolicySpec) { +func (in *RequestedBy) DeepCopyInto(out *RequestedBy) { *out = *in - if in.ProtectedLabels != nil { - in, out := &in.ProtectedLabels, &out.ProtectedLabels - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.PauseLabels != nil { - in, out := &in.PauseLabels, &out.PauseLabels + if in.Groups != nil { + in, out := &in.Groups, &out.Groups *out = make([]string, len(*in)) copy(*out, *in) } - out.OptInLabels = in.OptInLabels - out.AdvisoryLabels = in.AdvisoryLabels - if in.AllowedRepositoryPermissions != nil { - in, out := &in.AllowedRepositoryPermissions, &out.AllowedRepositoryPermissions + if in.Roles != nil { + in, out := &in.Roles, &out.Roles *out = make([]string, len(*in)) copy(*out, *in) } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorPolicySpec. -func (in *RepositoryMonitorPolicySpec) DeepCopy() *RepositoryMonitorPolicySpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestedBy. +func (in *RequestedBy) DeepCopy() *RequestedBy { if in == nil { return nil } - out := new(RepositoryMonitorPolicySpec) + out := new(RequestedBy) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorPullRequestCommandLabels) DeepCopyInto(out *RepositoryMonitorPullRequestCommandLabels) { +func (in *ResultReference) DeepCopyInto(out *ResultReference) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorPullRequestCommandLabels. -func (in *RepositoryMonitorPullRequestCommandLabels) DeepCopy() *RepositoryMonitorPullRequestCommandLabels { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResultReference. +func (in *ResultReference) DeepCopy() *ResultReference { if in == nil { return nil } - out := new(RepositoryMonitorPullRequestCommandLabels) + out := new(ResultReference) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorPullRequestTarget) DeepCopyInto(out *RepositoryMonitorPullRequestTarget) { +func (in *RetryPolicy) DeepCopyInto(out *RetryPolicy) { *out = *in - if in.Enabled != nil { - in, out := &in.Enabled, &out.Enabled - *out = new(bool) - **out = **in - } - if in.MaxPerRun != nil { - in, out := &in.MaxPerRun, &out.MaxPerRun - *out = new(int32) + if in.InitialDelay != nil { + in, out := &in.InitialDelay, &out.InitialDelay + *out = new(v1.Duration) **out = **in } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorPullRequestTarget. -func (in *RepositoryMonitorPullRequestTarget) DeepCopy() *RepositoryMonitorPullRequestTarget { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RetryPolicy. +func (in *RetryPolicy) DeepCopy() *RetryPolicy { if in == nil { return nil } - out := new(RepositoryMonitorPullRequestTarget) + out := new(RetryPolicy) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorRepairSpec) DeepCopyInto(out *RepositoryMonitorRepairSpec) { +func (in *RuntimePool) DeepCopyInto(out *RuntimePool) { *out = *in - if in.RequireMaintainerOptIn != nil { - in, out := &in.RequireMaintainerOptIn, &out.RequireMaintainerOptIn - *out = new(bool) - **out = **in - } - if in.MaxRepairsPerPR != nil { - in, out := &in.MaxRepairsPerPR, &out.MaxRepairsPerPR - *out = new(int32) - **out = **in - } - if in.MaxRepairsPerHead != nil { - in, out := &in.MaxRepairsPerHead, &out.MaxRepairsPerHead - *out = new(int32) - **out = **in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePool. +func (in *RuntimePool) DeepCopy() *RuntimePool { + if in == nil { + return nil } - if in.MaxValidationRetries != nil { - in, out := &in.MaxValidationRetries, &out.MaxValidationRetries - *out = new(int32) - **out = **in + out := new(RuntimePool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RuntimePool) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c } - if in.MaxReviewFixRetries != nil { - in, out := &in.MaxReviewFixRetries, &out.MaxReviewFixRetries - *out = new(int32) - **out = **in + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RuntimePoolActiveInstanceStatus) DeepCopyInto(out *RuntimePoolActiveInstanceStatus) { + *out = *in + if in.LastObservedTime != nil { + in, out := &in.LastObservedTime, &out.LastObservedTime + *out = (*in).DeepCopy() } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorRepairSpec. -func (in *RepositoryMonitorRepairSpec) DeepCopy() *RepositoryMonitorRepairSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolActiveInstanceStatus. +func (in *RuntimePoolActiveInstanceStatus) DeepCopy() *RuntimePoolActiveInstanceStatus { if in == nil { return nil } - out := new(RepositoryMonitorRepairSpec) + out := new(RuntimePoolActiveInstanceStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorReviewPublishInlineSpec) DeepCopyInto(out *RepositoryMonitorReviewPublishInlineSpec) { +func (in *RuntimePoolCapacityReservationStatus) DeepCopyInto(out *RuntimePoolCapacityReservationStatus) { *out = *in - if in.MaxComments != nil { - in, out := &in.MaxComments, &out.MaxComments - *out = new(int32) - **out = **in - } - if in.OnlyChangedLines != nil { - in, out := &in.OnlyChangedLines, &out.OnlyChangedLines - *out = new(bool) - **out = **in - } + in.ReservedAt.DeepCopyInto(&out.ReservedAt) + in.ExpiresAt.DeepCopyInto(&out.ExpiresAt) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorReviewPublishInlineSpec. -func (in *RepositoryMonitorReviewPublishInlineSpec) DeepCopy() *RepositoryMonitorReviewPublishInlineSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolCapacityReservationStatus. +func (in *RuntimePoolCapacityReservationStatus) DeepCopy() *RuntimePoolCapacityReservationStatus { if in == nil { return nil } - out := new(RepositoryMonitorReviewPublishInlineSpec) + out := new(RuntimePoolCapacityReservationStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorReviewPublishSpec) DeepCopyInto(out *RepositoryMonitorReviewPublishSpec) { +func (in *RuntimePoolCapacitySpec) DeepCopyInto(out *RuntimePoolCapacitySpec) { *out = *in - if in.PostPassed != nil { - in, out := &in.PostPassed, &out.PostPassed - *out = new(bool) - **out = **in - } - if in.PostNeedsChanges != nil { - in, out := &in.PostNeedsChanges, &out.PostNeedsChanges - *out = new(bool) - **out = **in - } - if in.PostNeedsHuman != nil { - in, out := &in.PostNeedsHuman, &out.PostNeedsHuman - *out = new(bool) - **out = **in - } - in.Inline.DeepCopyInto(&out.Inline) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorReviewPublishSpec. -func (in *RepositoryMonitorReviewPublishSpec) DeepCopy() *RepositoryMonitorReviewPublishSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolCapacitySpec. +func (in *RuntimePoolCapacitySpec) DeepCopy() *RuntimePoolCapacitySpec { if in == nil { return nil } - out := new(RepositoryMonitorReviewPublishSpec) + out := new(RuntimePoolCapacitySpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorReviewSpec) DeepCopyInto(out *RepositoryMonitorReviewSpec) { +func (in *RuntimePoolCapacityStatus) DeepCopyInto(out *RuntimePoolCapacityStatus) { *out = *in - if in.StaleReviewTTL != nil { - in, out := &in.StaleReviewTTL, &out.StaleReviewTTL - *out = new(v1.Duration) - **out = **in + if in.Reservations != nil { + in, out := &in.Reservations, &out.Reservations + *out = make([]RuntimePoolCapacityReservationStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - in.Publish.DeepCopyInto(&out.Publish) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorReviewSpec. -func (in *RepositoryMonitorReviewSpec) DeepCopy() *RepositoryMonitorReviewSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolCapacityStatus. +func (in *RuntimePoolCapacityStatus) DeepCopy() *RuntimePoolCapacityStatus { if in == nil { return nil } - out := new(RepositoryMonitorReviewSpec) + out := new(RuntimePoolCapacityStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorSpec) DeepCopyInto(out *RepositoryMonitorSpec) { +func (in *RuntimePoolList) DeepCopyInto(out *RuntimePoolList) { *out = *in - if in.GitSecretRef != nil { - in, out := &in.GitSecretRef, &out.GitSecretRef - *out = new(corev1.LocalObjectReference) - **out = **in - } - if in.TimeZone != nil { - in, out := &in.TimeZone, &out.TimeZone - *out = new(string) - **out = **in - } - if in.Suspend != nil { - in, out := &in.Suspend, &out.Suspend - *out = new(bool) - **out = **in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RuntimePool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - in.Targets.DeepCopyInto(&out.Targets) - out.Triggers = in.Triggers - in.Agents.DeepCopyInto(&out.Agents) - in.IssueWorkflow.DeepCopyInto(&out.IssueWorkflow) - in.Review.DeepCopyInto(&out.Review) - in.Repair.DeepCopyInto(&out.Repair) - in.Automerge.DeepCopyInto(&out.Automerge) - in.Policy.DeepCopyInto(&out.Policy) - in.Validation.DeepCopyInto(&out.Validation) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorSpec. -func (in *RepositoryMonitorSpec) DeepCopy() *RepositoryMonitorSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolList. +func (in *RuntimePoolList) DeepCopy() *RuntimePoolList { if in == nil { return nil } - out := new(RepositoryMonitorSpec) + out := new(RuntimePoolList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RuntimePoolList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorStatus) DeepCopyInto(out *RepositoryMonitorStatus) { +func (in *RuntimePoolProfileSpec) DeepCopyInto(out *RuntimePoolProfileSpec) { *out = *in - if in.LastRunTime != nil { - in, out := &in.LastRunTime, &out.LastRunTime - *out = (*in).DeepCopy() - } - if in.LastSuccessfulRunTime != nil { - in, out := &in.LastSuccessfulRunTime, &out.LastSuccessfulRunTime - *out = (*in).DeepCopy() - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) + if in.AdapterDigests != nil { + in, out := &in.AdapterDigests, &out.AdapterDigests + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorStatus. -func (in *RepositoryMonitorStatus) DeepCopy() *RepositoryMonitorStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolProfileSpec. +func (in *RuntimePoolProfileSpec) DeepCopy() *RuntimePoolProfileSpec { if in == nil { return nil } - out := new(RepositoryMonitorStatus) + out := new(RuntimePoolProfileSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorTargets) DeepCopyInto(out *RepositoryMonitorTargets) { +func (in *RuntimePoolRuntimeSpec) DeepCopyInto(out *RuntimePoolRuntimeSpec) { *out = *in - in.PullRequests.DeepCopyInto(&out.PullRequests) - in.Issues.DeepCopyInto(&out.Issues) - in.Commits.DeepCopyInto(&out.Commits) + in.Profile.DeepCopyInto(&out.Profile) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolRuntimeSpec. +func (in *RuntimePoolRuntimeSpec) DeepCopy() *RuntimePoolRuntimeSpec { + if in == nil { + return nil + } + out := new(RuntimePoolRuntimeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RuntimePoolSpec) DeepCopyInto(out *RuntimePoolSpec) { + *out = *in + out.TrustDomain = in.TrustDomain + in.Runtime.DeepCopyInto(&out.Runtime) + if in.Capacity != nil { + in, out := &in.Capacity, &out.Capacity + *out = new(RuntimePoolCapacitySpec) + **out = **in + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorTargets. -func (in *RepositoryMonitorTargets) DeepCopy() *RepositoryMonitorTargets { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolSpec. +func (in *RuntimePoolSpec) DeepCopy() *RuntimePoolSpec { if in == nil { return nil } - out := new(RepositoryMonitorTargets) + out := new(RuntimePoolSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorTriggers) DeepCopyInto(out *RepositoryMonitorTriggers) { +func (in *RuntimePoolStatus) DeepCopyInto(out *RuntimePoolStatus) { *out = *in - out.GitHub = in.GitHub + if in.ActiveInstance != nil { + in, out := &in.ActiveInstance, &out.ActiveInstance + *out = new(RuntimePoolActiveInstanceStatus) + (*in).DeepCopyInto(*out) + } + in.Capacity.DeepCopyInto(&out.Capacity) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorTriggers. -func (in *RepositoryMonitorTriggers) DeepCopy() *RepositoryMonitorTriggers { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolStatus. +func (in *RuntimePoolStatus) DeepCopy() *RuntimePoolStatus { if in == nil { return nil } - out := new(RepositoryMonitorTriggers) + out := new(RuntimePoolStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryMonitorValidationSpec) DeepCopyInto(out *RepositoryMonitorValidationSpec) { +func (in *RuntimePoolTrustDomain) DeepCopyInto(out *RuntimePoolTrustDomain) { *out = *in - if in.Commands != nil { - in, out := &in.Commands, &out.Commands - *out = make([]string, len(*in)) - copy(*out, *in) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryMonitorValidationSpec. -func (in *RepositoryMonitorValidationSpec) DeepCopy() *RepositoryMonitorValidationSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimePoolTrustDomain. +func (in *RuntimePoolTrustDomain) DeepCopy() *RuntimePoolTrustDomain { if in == nil { return nil } - out := new(RepositoryMonitorValidationSpec) + out := new(RuntimePoolTrustDomain) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryScan) DeepCopyInto(out *RepositoryScan) { +func (in *RuntimeSessionControl) DeepCopyInto(out *RuntimeSessionControl) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScan. -func (in *RepositoryScan) DeepCopy() *RepositoryScan { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeSessionControl. +func (in *RuntimeSessionControl) DeepCopy() *RuntimeSessionControl { if in == nil { return nil } - out := new(RepositoryScan) + out := new(RuntimeSessionControl) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RepositoryScan) DeepCopyObject() runtime.Object { +func (in *RuntimeSessionControl) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -2308,31 +3368,31 @@ func (in *RepositoryScan) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryScanList) DeepCopyInto(out *RepositoryScanList) { +func (in *RuntimeSessionControlList) DeepCopyInto(out *RuntimeSessionControlList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]RepositoryScan, len(*in)) + *out = make([]RuntimeSessionControl, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScanList. -func (in *RepositoryScanList) DeepCopy() *RepositoryScanList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeSessionControlList. +func (in *RuntimeSessionControlList) DeepCopy() *RuntimeSessionControlList { if in == nil { return nil } - out := new(RepositoryScanList) + out := new(RuntimeSessionControlList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RepositoryScanList) DeepCopyObject() runtime.Object { +func (in *RuntimeSessionControlList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -2340,153 +3400,63 @@ func (in *RepositoryScanList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryScanSpec) DeepCopyInto(out *RepositoryScanSpec) { - *out = *in - if in.GitSecretRef != nil { - in, out := &in.GitSecretRef, &out.GitSecretRef - *out = new(corev1.LocalObjectReference) - **out = **in - } - if in.TimeZone != nil { - in, out := &in.TimeZone, &out.TimeZone - *out = new(string) - **out = **in - } - if in.HistoryDays != nil { - in, out := &in.HistoryDays, &out.HistoryDays - *out = new(int32) - **out = **in - } - if in.ValidationMaxFindingsPerRun != nil { - in, out := &in.ValidationMaxFindingsPerRun, &out.ValidationMaxFindingsPerRun - *out = new(int32) - **out = **in - } - if in.CustomScanInstructionsRef != nil { - in, out := &in.CustomScanInstructionsRef, &out.CustomScanInstructionsRef - *out = new(PolicyConfigMapKeyRef) - **out = **in - } - if in.FalsePositivePolicyRef != nil { - in, out := &in.FalsePositivePolicyRef, &out.FalsePositivePolicyRef - *out = new(PolicyConfigMapKeyRef) - **out = **in - } - out.AnalysisAgentRef = in.AnalysisAgentRef - if in.PatchAgentRef != nil { - in, out := &in.PatchAgentRef, &out.PatchAgentRef - *out = new(AgentReference) - **out = **in - } - if in.MaxFindingsPerRun != nil { - in, out := &in.MaxFindingsPerRun, &out.MaxFindingsPerRun - *out = new(int32) - **out = **in - } - if in.Suspend != nil { - in, out := &in.Suspend, &out.Suspend - *out = new(bool) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScanSpec. -func (in *RepositoryScanSpec) DeepCopy() *RepositoryScanSpec { - if in == nil { - return nil - } - out := new(RepositoryScanSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RepositoryScanStatus) DeepCopyInto(out *RepositoryScanStatus) { +func (in *RuntimeSessionControlSpec) DeepCopyInto(out *RuntimeSessionControlSpec) { *out = *in - if in.LastScanAt != nil { - in, out := &in.LastScanAt, &out.LastScanAt - *out = (*in).DeepCopy() - } - if in.LastSuccessfulScanAt != nil { - in, out := &in.LastSuccessfulScanAt, &out.LastSuccessfulScanAt - *out = (*in).DeepCopy() - } - out.FindingCounts = in.FindingCounts - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + out.Owner = in.Owner } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryScanStatus. -func (in *RepositoryScanStatus) DeepCopy() *RepositoryScanStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeSessionControlSpec. +func (in *RuntimeSessionControlSpec) DeepCopy() *RuntimeSessionControlSpec { if in == nil { return nil } - out := new(RepositoryScanStatus) + out := new(RuntimeSessionControlSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RequestedBy) DeepCopyInto(out *RequestedBy) { +func (in *RuntimeSessionControlStatus) DeepCopyInto(out *RuntimeSessionControlStatus) { *out = *in - if in.Groups != nil { - in, out := &in.Groups, &out.Groups - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Roles != nil { - in, out := &in.Roles, &out.Roles - *out = make([]string, len(*in)) - copy(*out, *in) + if in.MutationLease != nil { + in, out := &in.MutationLease, &out.MutationLease + *out = new(RuntimeSessionMutationLeaseStatus) + (*in).DeepCopyInto(*out) } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestedBy. -func (in *RequestedBy) DeepCopy() *RequestedBy { - if in == nil { - return nil + if in.VerifiedBaseline != nil { + in, out := &in.VerifiedBaseline, &out.VerifiedBaseline + *out = new(ControlVerifiedBranchBaseline) + **out = **in } - out := new(RequestedBy) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResultReference) DeepCopyInto(out *ResultReference) { - *out = *in + in.ControlRecordMutationStatus.DeepCopyInto(&out.ControlRecordMutationStatus) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResultReference. -func (in *ResultReference) DeepCopy() *ResultReference { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeSessionControlStatus. +func (in *RuntimeSessionControlStatus) DeepCopy() *RuntimeSessionControlStatus { if in == nil { return nil } - out := new(ResultReference) + out := new(RuntimeSessionControlStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RetryPolicy) DeepCopyInto(out *RetryPolicy) { +func (in *RuntimeSessionMutationLeaseStatus) DeepCopyInto(out *RuntimeSessionMutationLeaseStatus) { *out = *in - if in.InitialDelay != nil { - in, out := &in.InitialDelay, &out.InitialDelay - *out = new(v1.Duration) - **out = **in + in.AcquiredAt.DeepCopyInto(&out.AcquiredAt) + if in.ExpiresAt != nil { + in, out := &in.ExpiresAt, &out.ExpiresAt + *out = (*in).DeepCopy() } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RetryPolicy. -func (in *RetryPolicy) DeepCopy() *RetryPolicy { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeSessionMutationLeaseStatus. +func (in *RuntimeSessionMutationLeaseStatus) DeepCopy() *RuntimeSessionMutationLeaseStatus { if in == nil { return nil } - out := new(RetryPolicy) + out := new(RuntimeSessionMutationLeaseStatus) in.DeepCopyInto(out) return out } @@ -2886,22 +3856,59 @@ func (in *Task) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TaskExecutionOutcome) DeepCopyInto(out *TaskExecutionOutcome) { +func (in *TaskDeliveryStatus) DeepCopyInto(out *TaskDeliveryStatus) { *out = *in - if in.ResultRef != nil { - in, out := &in.ResultRef, &out.ResultRef - *out = new(ResultReference) + if in.SourceRepository != nil { + in, out := &in.SourceRepository, &out.SourceRepository + *out = new(RepositoryIdentity) **out = **in } - in.RecordedAt.DeepCopyInto(&out.RecordedAt) + if in.PublicationRepository != nil { + in, out := &in.PublicationRepository, &out.PublicationRepository + *out = new(RepositoryIdentity) + **out = **in + } + if in.RemoteBeforeSHA != nil { + in, out := &in.RemoteBeforeSHA, &out.RemoteBeforeSHA + *out = new(string) + **out = **in + } + if in.PRReceipt != nil { + in, out := &in.PRReceipt, &out.PRReceipt + *out = new(TaskPullRequestReceipt) + **out = **in + } + if in.LastTransitionTime != nil { + in, out := &in.LastTransitionTime, &out.LastTransitionTime + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskDeliveryStatus. +func (in *TaskDeliveryStatus) DeepCopy() *TaskDeliveryStatus { + if in == nil { + return nil + } + out := new(TaskDeliveryStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TaskExecutionStatus) DeepCopyInto(out *TaskExecutionStatus) { + *out = *in + if in.LastTransitionTime != nil { + in, out := &in.LastTransitionTime, &out.LastTransitionTime + *out = (*in).DeepCopy() + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskExecutionOutcome. -func (in *TaskExecutionOutcome) DeepCopy() *TaskExecutionOutcome { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskExecutionStatus. +func (in *TaskExecutionStatus) DeepCopy() *TaskExecutionStatus { if in == nil { return nil } - out := new(TaskExecutionOutcome) + out := new(TaskExecutionStatus) in.DeepCopyInto(out) return out } @@ -2938,6 +3945,21 @@ func (in *TaskList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TaskPullRequestReceipt) DeepCopyInto(out *TaskPullRequestReceipt) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskPullRequestReceipt. +func (in *TaskPullRequestReceipt) DeepCopy() *TaskPullRequestReceipt { + if in == nil { + return nil + } + out := new(TaskPullRequestReceipt) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TaskSpec) DeepCopyInto(out *TaskSpec) { *out = *in @@ -3077,9 +4099,19 @@ func (in *TaskStatus) DeepCopyInto(out *TaskStatus) { *out = new(ResultReference) **out = **in } + if in.Execution != nil { + in, out := &in.Execution, &out.Execution + *out = new(TaskExecutionStatus) + (*in).DeepCopyInto(*out) + } + if in.Delivery != nil { + in, out := &in.Delivery, &out.Delivery + *out = new(TaskDeliveryStatus) + (*in).DeepCopyInto(*out) + } if in.ExecutionOutcome != nil { in, out := &in.ExecutionOutcome, &out.ExecutionOutcome - *out = new(TaskExecutionOutcome) + *out = new(TaskWorkloadExecutionOutcome) (*in).DeepCopyInto(*out) } if in.ExecutionWorkspace != nil { @@ -3087,11 +4119,6 @@ func (in *TaskStatus) DeepCopyInto(out *TaskStatus) { *out = new(ExecutionWorkspaceStatus) (*in).DeepCopyInto(*out) } - if in.HarnessRuntime != nil { - in, out := &in.HarnessRuntime, &out.HarnessRuntime - *out = new(HarnessRuntimeStatus) - **out = **in - } if in.ChildTasks != nil { in, out := &in.ChildTasks, &out.ChildTasks *out = make([]ChildTaskStatus, len(*in)) @@ -3156,6 +4183,27 @@ func (in *TaskTransaction) DeepCopy() *TaskTransaction { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TaskWorkloadExecutionOutcome) DeepCopyInto(out *TaskWorkloadExecutionOutcome) { + *out = *in + if in.ResultRef != nil { + in, out := &in.ResultRef, &out.ResultRef + *out = new(ResultReference) + **out = **in + } + in.RecordedAt.DeepCopyInto(&out.RecordedAt) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskWorkloadExecutionOutcome. +func (in *TaskWorkloadExecutionOutcome) DeepCopy() *TaskWorkloadExecutionOutcome { + if in == nil { + return nil + } + out := new(TaskWorkloadExecutionOutcome) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Tool) DeepCopyInto(out *Tool) { *out = *in @@ -3372,11 +4420,46 @@ func (in *WorkspaceClassReference) DeepCopy() *WorkspaceClassReference { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceConfig) DeepCopyInto(out *WorkspaceConfig) { *out = *in - if in.GitSecretRef != nil { - in, out := &in.GitSecretRef, &out.GitSecretRef - *out = new(corev1.LocalObjectReference) + if in.SourceRepository != nil { + in, out := &in.SourceRepository, &out.SourceRepository + *out = new(RepositoryIdentity) + **out = **in + } + if in.ReadCredentialRef != nil { + in, out := &in.ReadCredentialRef, &out.ReadCredentialRef + *out = new(WorkspaceCredentialReference) + **out = **in + } + if in.PublicationRepository != nil { + in, out := &in.PublicationRepository, &out.PublicationRepository + *out = new(RepositoryIdentity) + **out = **in + } + if in.PublicationReadCredentialRef != nil { + in, out := &in.PublicationReadCredentialRef, &out.PublicationReadCredentialRef + *out = new(WorkspaceCredentialReference) + **out = **in + } + if in.PublicationCredentialRef != nil { + in, out := &in.PublicationCredentialRef, &out.PublicationCredentialRef + *out = new(WorkspaceCredentialReference) + **out = **in + } + if in.ForgeCredentialRef != nil { + in, out := &in.ForgeCredentialRef, &out.ForgeCredentialRef + *out = new(WorkspaceCredentialReference) + **out = **in + } + if in.MaxChangedFiles != nil { + in, out := &in.MaxChangedFiles, &out.MaxChangedFiles + *out = new(int32) **out = **in } + if in.AllowedPaths != nil { + in, out := &in.AllowedPaths, &out.AllowedPaths + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceConfig. @@ -3389,6 +4472,21 @@ func (in *WorkspaceConfig) DeepCopy() *WorkspaceConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceCredentialReference) DeepCopyInto(out *WorkspaceCredentialReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceCredentialReference. +func (in *WorkspaceCredentialReference) DeepCopy() *WorkspaceCredentialReference { + if in == nil { + return nil + } + out := new(WorkspaceCredentialReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceObjectReference) DeepCopyInto(out *WorkspaceObjectReference) { *out = *in diff --git a/charts/orka/README.md b/charts/orka/README.md index c6556204d..1b171593a 100644 --- a/charts/orka/README.md +++ b/charts/orka/README.md @@ -2,7 +2,7 @@ This chart is generated from `cmd/build/helmify`; edit the generator inputs and run `make manifests` rather than editing generated chart copies directly. It -packages all twelve canonical Orka CRDs under `crds/`. +packages all 26 canonical Orka CRDs under `crds/`. ## Fresh install @@ -15,6 +15,19 @@ helm install orka charts/orka \ --wait ``` +The provider proxy is disabled by default. Before enabling `providerProxy.enabled=true` (required when `controller.acpRuntime.enabled=true`), install Vekil in `vekil-system`; the chart then installs the exact cross-namespace ingress policy there. The chart-managed provider proxy itself always runs in the Helm release namespace. Leave `controller.acpRuntime.providerProxyNamespace` empty or set it to that release namespace. The only supported upstream is `http://vekil.vekil-system.svc:1337` (an optional trailing slash is normalized); alternate hosts, namespaces, and ports are rejected because the chart does not create matching NetworkPolicies. + +`service.port` is the controller Service port used by controller and Publisher Service URLs. `controller.apiPort` is only the controller container listener and Service target port. + +### Coordinated authentication Secret rotation + +The Publisher and SCM egress proxy read their authentication material at process startup. Rotate each Secret and its non-secret rollout marker in the same Helm upgrade: + +- When rotating `publisher.auth.existingSecret` (or the chart-managed publisher auth values), bump `publisher.auth.rolloutNonce`. The marker is added only to the controller and Publisher Pod templates so both restart onto the same credential generation. +- When rotating `scmEgressProxy.auth.existingSecret` (or the chart-managed SCM proxy token), bump `scmEgressProxy.auth.rolloutNonce`. The marker is added only to the Publisher and SCM proxy Pod templates. + +The nonce is a revision label, not a credential. Never put Secret content in it. A coordinated upgrade may briefly fail closed while Pods roll, but it avoids an indefinite split generation. + CRDs are cluster-scoped and shared by every Orka release. Use `--skip-crds` only when a designated platform or GitOps workflow already manages compatible Orka CRDs for the cluster. @@ -76,7 +89,7 @@ A matching Orka source checkout provides the same guarded flow as competing CRD apply workflows for the same cluster. If another system owns the CRDs, perform the CRD-first step through that system, -wait for all twelve CRDs to become `Established`, and then upgrade Orka. +wait for all 26 CRDs to become `Established`, and then upgrade Orka. If a previous release was uninstalled, update its retained CRDs first and install the replacement release with `--skip-crds`. diff --git a/charts/orka/crds/agent-customresourcedefinition.yaml b/charts/orka/crds/agent-customresourcedefinition.yaml index cfa5071b5..bc5723087 100644 --- a/charts/orka/crds/agent-customresourcedefinition.yaml +++ b/charts/orka/crds/agent-customresourcedefinition.yaml @@ -1094,10 +1094,10 @@ spec: type: array workspace: description: |- - Workspace requests an upstream agent-sandbox execution workspace for agent Tasks. - When enabled, the Task controller validates the request and propagates the - resolved sandbox settings to the agent worker Job. The worker wrapper then - claims the sandbox workspace and runs the configured agent runtime inside it. + Workspace requests an execution workspace for worker-backed Task types. + ACP core agent Tasks reject this field because their ephemeral workspace is + owned by RuntimeSession lifecycle and clean-room publication. Actor-backed + RuntimeSession support is a future integration behind the v2 lifecycle seam. properties: boot: description: |- @@ -1105,6 +1105,18 @@ spec: instead of resuming from the provider's default snapshot. Currently supported by the Substrate provider. type: boolean + classRef: + description: |- + ClassRef selects an immutable ExecutionWorkspaceClass in the Task namespace. Setting + classRef implicitly enables the controller-first workspace path. + properties: + name: + description: Name is the class name. + minLength: 1 + type: string + required: + - name + type: object cleanupPolicy: description: |- CleanupPolicy controls whether the workspace is deleted or retained after use. @@ -1138,6 +1150,13 @@ spec: worker derives a stable key from namespace, template, and reuse key. type: string type: object + onDetach: + description: OnDetach requests an action allowed by the selected + class. + enum: + - Suspend + - Delete + type: string poolRef: description: |- PoolRef references an operator-managed Substrate actor pool for placement, @@ -1206,7 +1225,22 @@ spec: It defaults to the Task namespace, or the controller namespace when configured. type: string type: object + workspaceSlot: + default: default + description: WorkspaceSlot names one independently reusable + workspace within a Session. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string type: object + x-kubernetes-validations: + - message: classRef cannot be combined with legacy enabled, provider, + template, pool, cleanup, boot, snapshot, or hibernation settings + rule: '!has(self.classRef) || (!has(self.provider) && !has(self.templateRef) + && !has(self.poolRef) && (!has(self.enabled) || !self.enabled) + && !has(self.cleanupPolicy) && (!has(self.boot) || !self.boot) + && !has(self.snapshot) && !has(self.hibernation))' type: object model: description: |- @@ -1367,6 +1401,17 @@ spec: maximum: 1000 minimum: 1 type: integer + defaultReasoningEffort: + description: |- + DefaultReasoningEffort configures the CLI runtime reasoning effort for tasks using this Agent. + Runtime adapters reject values they do not support (for example, Codex does not support max). + enum: + - low + - medium + - high + - xhigh + - max + type: string runtimeRef: description: RuntimeRef selects an admin-governed AgentRuntime for custom/BYO harness runtimes. @@ -1383,10 +1428,9 @@ spec: description: Type specifies which built-in CLI runtime to use. Use runtimeRef for admin-registered custom runtimes. enum: - - copilot - claude - codex - - opencode + - copilot type: string type: object x-kubernetes-validations: @@ -1499,6 +1543,9 @@ spec: Zero means the agent is never auto-deleted (permanent). Default is no TTL (permanent). type: string type: object + x-kubernetes-validations: + - message: execution.workspace.classRef is only supported on Task specs + rule: '!has(self.execution) || !has(self.execution.workspace) || !has(self.execution.workspace.classRef)' status: description: AgentStatus defines the observed state of Agent properties: diff --git a/charts/orka/crds/agentruntime-customresourcedefinition.yaml b/charts/orka/crds/agentruntime-customresourcedefinition.yaml index e9623b7ae..5c4b43c6d 100644 --- a/charts/orka/crds/agentruntime-customresourcedefinition.yaml +++ b/charts/orka/crds/agentruntime-customresourcedefinition.yaml @@ -24,8 +24,8 @@ spec: - jsonPath: .spec.deployment.mode name: Mode type: string - - jsonPath: .status.observedCapabilities.runtimeName - name: Runtime + - jsonPath: .status.observedCapabilities.runtimeInstanceID + name: Instance type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -33,7 +33,8 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: AgentRuntime is the Schema for registered Orka harness runtimes. + description: AgentRuntime is the Schema for registered external Orka harness + runtimes. properties: apiVersion: description: |- @@ -57,96 +58,306 @@ spec: Orka harness runtime. properties: capabilities: - description: Capabilities declares readiness requirements Orka checks - against the runtime. + description: Capabilities pins the exact instance, profile, limits, + and governance claims. properties: - brokeredToolClasses: - description: BrokeredToolClasses lists brokered tool classes the - runtime must advertise when brokered mode is required. - items: - description: AgentRuntimeBrokeredToolClass declares which classes - of Orka-brokered tools a runtime can request. - enum: - - read - - write - - coordination - type: string - type: array - x-kubernetes-list-type: set - supportsArtifacts: - description: SupportsArtifacts requires the runtime to advertise - artifact/result reference support when true. - type: boolean - supportsCancel: - description: SupportsCancel requires the runtime to advertise - cancellation support when true. - type: boolean - supportsContinuation: - description: SupportsContinuation requires the runtime to advertise - continuation after Orka-brokered tool results when true. + limits: + description: Limits must exactly match /v2/capabilities. + properties: + maxBufferedEvents: + format: int32 + minimum: 1 + type: integer + maxConcurrentPrompts: + format: int32 + minimum: 1 + type: integer + maxEventLineBytes: + format: int32 + minimum: 1 + type: integer + maxPendingPermissions: + format: int32 + minimum: 1 + type: integer + maxPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + maxRequestBytes: + format: int32 + minimum: 1 + type: integer + maxResidentSessions: + format: int32 + minimum: 1 + type: integer + maxTerminalResultBytes: + format: int32 + minimum: 1 + type: integer + maxUpdateEventsPerSecond: + format: int32 + minimum: 1 + type: integer + maxWorkspaceDeltaBytes: + format: int64 + minimum: 1 + type: integer + minPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + required: + - maxBufferedEvents + - maxConcurrentPrompts + - maxEventLineBytes + - maxPendingPermissions + - maxPromptLeaseMillis + - maxRequestBytes + - maxResidentSessions + - maxTerminalResultBytes + - maxUpdateEventsPerSecond + - maxWorkspaceDeltaBytes + - minPromptLeaseMillis + type: object + profile: + description: Profile is the exact immutable profile accepted by + session creation. + properties: + acpProfile: + description: ACPProfile is the reviewed ACP profile. + enum: + - acp.v1 + type: string + adapterDigest: + description: AdapterDigest pins the adapter/CLI artifact set. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + adapterName: + description: AdapterName identifies the sole adapter contained + by this external profile. + maxLength: 128 + minLength: 1 + type: string + agentConfigurationDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + approvalPolicyDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digest: + description: Digest is the canonical orka.harness.v2 runtime-profile + digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digestSchemaVersion: + description: DigestSchemaVersion identifies the canonical + profile digest schema. + enum: + - 1 + format: int32 + type: integer + mcpConfigurationDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + model: + maxLength: 256 + minLength: 1 + type: string + providerKind: + maxLength: 128 + minLength: 1 + type: string + proxyCredentialRole: + maxLength: 256 + minLength: 1 + type: string + proxyCredentialScope: + maxLength: 1024 + minLength: 1 + type: string + resourceClass: + maxLength: 128 + minLength: 1 + type: string + toolPolicyDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + workspaceIntent: + description: WorkspaceIntent is the one immutable strict intent + represented by this profile. + enum: + - read + - write + type: string + required: + - acpProfile + - adapterDigest + - adapterName + - agentConfigurationDigest + - approvalPolicyDigest + - digest + - digestSchemaVersion + - mcpConfigurationDigest + - model + - providerKind + - proxyCredentialRole + - proxyCredentialScope + - resourceClass + - toolPolicyDigest + - workspaceIntent + type: object + runtimeInstanceID: + description: |- + RuntimeInstanceID is the immutable external supervisor instance expected from + authenticated /v2/status and every conformance response. + maxLength: 253 + minLength: 1 + type: string + supportsDrain: + description: SupportsDrain must exactly match the static capability + claim. type: boolean - supportsRuntimeSessions: - description: SupportsRuntimeSessions requires the runtime to advertise - stable runtime sessions when true. + supportsPublicationFinalization: + description: SupportsPublicationFinalization must exactly match + the static capability claim. type: boolean - toolExecutionModes: - description: ToolExecutionModes lists tool execution modes the - runtime must advertise. - items: - description: AgentRuntimeToolExecutionMode declares how custom - runtimes interact with tools. - enum: - - observed - - brokered - type: string - type: array - x-kubernetes-list-type: set + workspaceGovernance: + description: WorkspaceGovernance must exactly match the static + capability claim. + properties: + cancellationSettlement: + type: boolean + duplicateSafeMutations: + type: boolean + exactInstanceFencing: + type: boolean + mode: + description: Mode selects strict Orka governance or an explicit + trusted escape hatch. + enum: + - strict-governed + - trusted-non-governed + type: string + noDirectSCMPublication: + type: boolean + orkaOwnedCleanRoomPublication: + type: boolean + orkaOwnedWorkspaceDeltas: + type: boolean + promptScopedBrokerAuthorization: + type: boolean + trusted: + description: |- + Trusted must be true only for trusted-non-governed runtimes. Such runtimes + are ineligible for Tasks requesting strict read or write guarantees. + type: boolean + required: + - cancellationSettlement + - duplicateSafeMutations + - exactInstanceFencing + - mode + - noDirectSCMPublication + - orkaOwnedCleanRoomPublication + - orkaOwnedWorkspaceDeltas + - promptScopedBrokerAuthorization + - trusted + type: object + x-kubernetes-validations: + - message: trusted-non-governed runtimes must be explicitly marked + trusted + rule: self.mode != 'trusted-non-governed' || self.trusted + - message: strict-governed runtimes must not use the trusted non-governed + escape hatch + rule: self.mode != 'strict-governed' || !self.trusted + - message: strict-governed runtimes must claim every strict workspace + governance guarantee + rule: self.mode != 'strict-governed' || (self.orkaOwnedWorkspaceDeltas + && self.promptScopedBrokerAuthorization && self.noDirectSCMPublication + && self.orkaOwnedCleanRoomPublication && self.exactInstanceFencing + && self.duplicateSafeMutations && self.cancellationSettlement) + - message: trusted-non-governed runtimes must not claim strict + workspace guarantees + rule: self.mode != 'trusted-non-governed' || (!self.orkaOwnedWorkspaceDeltas + && !self.promptScopedBrokerAuthorization && !self.noDirectSCMPublication + && !self.orkaOwnedCleanRoomPublication && !self.exactInstanceFencing + && !self.duplicateSafeMutations && !self.cancellationSettlement) + required: + - limits + - profile + - runtimeInstanceID + - supportsDrain + - workspaceGovernance type: object clientAuth: - description: ClientAuth configures controller-to-runtime authentication. + description: ClientAuth configures controller authentication and mutation + authorization. properties: - bearerTokenSecretRef: + controllerBearerTokenSecretRef: + description: |- + ControllerBearerTokenSecretRef supplies the controller bearer token used by + authenticated v2 status and mutation endpoints. + properties: + key: + description: Key is the Secret data key. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is the Secret name in the AgentRuntime namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + operationCapabilitySecretRef: description: |- - BearerAuthRef points to the bearer token Secret used for mutating harness endpoints. - The referenced Secret must opt in with label orka.ai/agent-runtime-auth=true, - may set orka.ai/agent-runtime-name= to restrict use to one AgentRuntime, - and must set annotation orka.ai/agent-runtime-endpoint= to bind the token to one endpoint. + OperationCapabilitySecretRef supplies the HMAC secret used to bind every + mutation to its exact fence, operation identity, request digest, and expiry. properties: key: - description: Key is the Secret data key containing the bearer - token. + description: Key is the Secret data key. + maxLength: 253 minLength: 1 type: string name: - description: Name is the Secret name. + description: Name is the Secret name in the AgentRuntime namespace. + maxLength: 253 minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - key - name type: object required: - - bearerTokenSecretRef + - controllerBearerTokenSecretRef + - operationCapabilitySecretRef type: object contractVersion: - default: orka.harness.v1 + default: orka.harness.v2 description: ContractVersion is the Orka harness contract this runtime must implement. enum: - - orka.harness.v1 + - orka.harness.v2 type: string deployment: description: Deployment identifies the runtime endpoint provider. properties: endpoint: description: |- - Endpoint is the base URL for a pre-deployed or external orka.harness.v1 service. - It must not contain credentials; bearer auth is configured via clientAuth. + Endpoint is the base URL for an external orka.harness.v2 service. It must + not contain credentials, query parameters, or fragments. pattern: ^https?://[^\s@?#]+$ type: string mode: - description: Mode is the deployment mode. The first milestone - supports external endpoints only. + description: |- + Mode is the deployment mode. External AgentRuntime registrations are not + scaled or recycled by Orka. enum: - external-endpoint type: string @@ -155,6 +366,7 @@ spec: - mode type: object required: + - capabilities - clientAuth - contractVersion - deployment @@ -230,101 +442,185 @@ spec: message: description: Message provides sanitized readiness context. type: string - observedAuthRefResourceVersion: - description: |- - ObservedAuthRefResourceVersion is the resourceVersion of the bearer auth Secret - used for the last readiness probe. It is non-secret metadata used to decide - when token rotation requires a fresh authenticated conformance turn. - type: string observedCapabilities: description: ObservedCapabilities contains sanitized capabilities from the last probe. properties: - brokeredToolClasses: - description: BrokeredToolClasses are the brokered tool classes - advertised by /v1/capabilities. - items: - description: AgentRuntimeBrokeredToolClass declares which classes - of Orka-brokered tools a runtime can request. - enum: - - read - - write - - coordination - type: string - type: array - x-kubernetes-list-type: set - maxConcurrentTurns: - description: MaxConcurrentTurns is the advertised concurrency - ceiling. - type: integer - maxOutputBytes: - description: MaxOutputBytes is the advertised maximum output payload - size. + acpVersion: + type: string + adapterDigest: + type: string + adapterName: + type: string + controllerEpoch: format: int64 type: integer - maxTurnSeconds: - description: MaxTurnSeconds is the advertised per-turn duration - ceiling. + lifecycle: + type: string + limits: + description: AgentRuntimeProtocolLimits pins the exact bounded + v2 limits expected from the runtime. + properties: + maxBufferedEvents: + format: int32 + minimum: 1 + type: integer + maxConcurrentPrompts: + format: int32 + minimum: 1 + type: integer + maxEventLineBytes: + format: int32 + minimum: 1 + type: integer + maxPendingPermissions: + format: int32 + minimum: 1 + type: integer + maxPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + maxRequestBytes: + format: int32 + minimum: 1 + type: integer + maxResidentSessions: + format: int32 + minimum: 1 + type: integer + maxTerminalResultBytes: + format: int32 + minimum: 1 + type: integer + maxUpdateEventsPerSecond: + format: int32 + minimum: 1 + type: integer + maxWorkspaceDeltaBytes: + format: int64 + minimum: 1 + type: integer + minPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + required: + - maxBufferedEvents + - maxConcurrentPrompts + - maxEventLineBytes + - maxPendingPermissions + - maxPromptLeaseMillis + - maxRequestBytes + - maxResidentSessions + - maxTerminalResultBytes + - maxUpdateEventsPerSecond + - maxWorkspaceDeltaBytes + - minPromptLeaseMillis + type: object + model: + type: string + profileDigestSchemaVersion: + format: int32 type: integer protocolVersion: - description: ProtocolVersion is the runtime's advertised Orka - protocol version. type: string providerKind: - description: ProviderKind is the provider kind advertised by /v1/capabilities. type: string - runtimeName: - description: RuntimeName is the runtime name advertised by /v1/capabilities. + runtimeInstanceID: type: string - runtimeVersion: - description: RuntimeVersion is the runtime version advertised - by /v1/capabilities. + runtimePoolGeneration: + format: int64 + type: integer + runtimePoolUID: type: string - supportsArtifacts: - description: SupportsArtifacts reports whether the runtime advertises - artifact/result reference support. - type: boolean - supportsCancel: - description: SupportsCancel reports whether the runtime advertises - cancellation support. - type: boolean - supportsContinuation: - description: SupportsContinuation reports whether the runtime - advertises continuation support. - type: boolean - supportsRuntimeSessions: - description: SupportsRuntimeSessions reports whether the runtime - advertises runtime-session support. - type: boolean - supportsSuspend: - description: SupportsSuspend reports whether the runtime advertises - suspend support. + runtimeProfileDigest: + type: string + supervisorBootID: + type: string + supportsDrain: type: boolean - supportsWorkspaceSnapshot: - description: SupportsWorkspaceSnapshot reports whether the runtime - advertises workspace snapshots. + supportsPublicationFinalization: type: boolean - toolExecutionModes: - description: ToolExecutionModes are the tool modes advertised - by /v1/capabilities. - items: - description: AgentRuntimeToolExecutionMode declares how custom - runtimes interact with tools. - enum: - - observed - - brokered - type: string - type: array - x-kubernetes-list-type: set transport: - description: Transport is the runtime transport, normally http+sse. type: string + workspaceGovernance: + description: |- + AgentRuntimeWorkspaceGovernanceCapabilities are static claims advertised by + /v2/capabilities and exercised by the hostile conformance cycle. + properties: + cancellationSettlement: + type: boolean + duplicateSafeMutations: + type: boolean + exactInstanceFencing: + type: boolean + mode: + description: Mode selects strict Orka governance or an explicit + trusted escape hatch. + enum: + - strict-governed + - trusted-non-governed + type: string + noDirectSCMPublication: + type: boolean + orkaOwnedCleanRoomPublication: + type: boolean + orkaOwnedWorkspaceDeltas: + type: boolean + promptScopedBrokerAuthorization: + type: boolean + trusted: + description: |- + Trusted must be true only for trusted-non-governed runtimes. Such runtimes + are ineligible for Tasks requesting strict read or write guarantees. + type: boolean + required: + - cancellationSettlement + - duplicateSafeMutations + - exactInstanceFencing + - mode + - noDirectSCMPublication + - orkaOwnedCleanRoomPublication + - orkaOwnedWorkspaceDeltas + - promptScopedBrokerAuthorization + - trusted + type: object + x-kubernetes-validations: + - message: trusted-non-governed runtimes must be explicitly marked + trusted + rule: self.mode != 'trusted-non-governed' || self.trusted + - message: strict-governed runtimes must not use the trusted non-governed + escape hatch + rule: self.mode != 'strict-governed' || !self.trusted + - message: strict-governed runtimes must claim every strict workspace + governance guarantee + rule: self.mode != 'strict-governed' || (self.orkaOwnedWorkspaceDeltas + && self.promptScopedBrokerAuthorization && self.noDirectSCMPublication + && self.orkaOwnedCleanRoomPublication && self.exactInstanceFencing + && self.duplicateSafeMutations && self.cancellationSettlement) + - message: trusted-non-governed runtimes must not claim strict + workspace guarantees + rule: self.mode != 'trusted-non-governed' || (!self.orkaOwnedWorkspaceDeltas + && !self.promptScopedBrokerAuthorization && !self.noDirectSCMPublication + && !self.orkaOwnedCleanRoomPublication && !self.exactInstanceFencing + && !self.duplicateSafeMutations && !self.cancellationSettlement) type: object + observedControllerAuthRefResourceVersion: + description: |- + ObservedControllerAuthRefResourceVersion is the bearer Secret version used + by the last successful or failed authenticated conformance probe. + type: string observedGeneration: description: ObservedGeneration is the latest generation reconciled into this status. format: int64 type: integer + observedOperationCapabilityRefResourceVersion: + description: |- + ObservedOperationCapabilityRefResourceVersion is the HMAC Secret version used + by the last mutation conformance probe. + type: string ready: description: Ready indicates the runtime passed the configured Orka readiness checks. diff --git a/charts/orka/crds/branchclaim-customresourcedefinition.yaml b/charts/orka/crds/branchclaim-customresourcedefinition.yaml new file mode 100644 index 000000000..1328a8fe0 --- /dev/null +++ b/charts/orka/crds/branchclaim-customresourcedefinition.yaml @@ -0,0 +1,197 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: branchclaims.core.orka.ai +spec: + group: core.orka.ai + names: + kind: BranchClaim + listKind: BranchClaimList + plural: branchclaims + shortNames: + - bclaim + singular: branchclaim + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.repositoryId + name: Repository + type: string + - jsonPath: .spec.ref + name: Ref + type: string + - jsonPath: .spec.ownerKind + name: Owner + type: string + - jsonPath: .status.generation + name: Generation + type: integer + - jsonPath: .status.availability + name: Availability + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + BranchClaim is the cluster-wide Kubernetes-authoritative ownership and exact + baseline record for one canonical repository branch. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: BranchClaimSpec is the immutable repository/ref ownership + identity. + properties: + id: + maxLength: 1024 + minLength: 1 + type: string + ownerKind: + description: BranchClaimOwnerKind identifies the durable owner of + an Orka-managed branch. + enum: + - Task + - Session + type: string + ownerUid: + maxLength: 1024 + minLength: 1 + type: string + ref: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + repositoryId: + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + required: + - id + - ownerKind + - ownerUid + - ref + - repositoryId + - requestDigest + type: object + x-kubernetes-validations: + - message: branch claim spec is immutable + rule: self == oldSelf + status: + description: BranchClaimStatus is the exact generation, baseline, and + availability CAS. + properties: + availability: + description: BranchClaimAvailability gates further branch mutation. + enum: + - Available + - ReconciliationBlocked + type: string + blockedReason: + maxLength: 16384 + type: string + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + generation: + format: int64 + minimum: 1 + type: integer + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + lastVerified: + description: LastVerified is the independently observed exact target + ref. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + relatedPublicationId: + maxLength: 1024 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: available branch claims must clear block metadata + rule: '!has(self.availability) || self.availability != ''Available'' + || ((!has(self.blockedReason) || size(self.blockedReason) == 0) && + (!has(self.relatedPublicationId) || size(self.relatedPublicationId) + == 0))' + - message: reconciliation-blocked branch claims require a reason + rule: '!has(self.availability) || self.availability != ''ReconciliationBlocked'' + || (has(self.blockedReason) && size(self.blockedReason) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/controllerepoch-customresourcedefinition.yaml b/charts/orka/crds/controllerepoch-customresourcedefinition.yaml new file mode 100644 index 000000000..9d1c486f2 --- /dev/null +++ b/charts/orka/crds/controllerepoch-customresourcedefinition.yaml @@ -0,0 +1,109 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: controllerepochs.core.orka.ai +spec: + group: core.orka.ai + names: + kind: ControllerEpoch + listKind: ControllerEpochList + plural: controllerepochs + shortNames: + - cepoch + singular: controllerepoch + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.epoch + name: Epoch + type: integer + - jsonPath: .status.holderId + name: Holder + type: string + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ControllerEpoch is the human-visible Kubernetes control record paired with + an authoritative namespaced Lease. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ControllerEpochSpec is the immutable epoch-domain identity. The associated + coordination.k8s.io Lease is the CAS authority for holder and epoch changes. + properties: + name: + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: controller epoch spec is immutable + rule: self == oldSelf + status: + description: |- + ControllerEpochStatus mirrors the authoritative Lease state for inspection + and recovery. LeaseResourceVersion identifies the exact Lease revision. + properties: + acquiredAt: + format: date-time + type: string + epoch: + format: int64 + minimum: 1 + type: integer + holderId: + maxLength: 1024 + type: string + leaseName: + maxLength: 253 + type: string + leaseResourceVersion: + maxLength: 64 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + updatedAt: + format: date-time + type: string + version: + format: int64 + minimum: 1 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/executionworkspace-customresourcedefinition.yaml b/charts/orka/crds/executionworkspace-customresourcedefinition.yaml new file mode 100644 index 000000000..0afd7fc0c --- /dev/null +++ b/charts/orka/crds/executionworkspace-customresourcedefinition.yaml @@ -0,0 +1,783 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: executionworkspaces.workspace.orka.ai +spec: + group: workspace.orka.ai + names: + categories: + - orka + kind: ExecutionWorkspace + listKind: ExecutionWorkspaceList + plural: executionworkspaces + shortNames: + - ew + singular: executionworkspace + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.classBinding.name + name: Class + type: string + - jsonPath: .spec.providerBinding.name + name: Provider + type: string + - jsonPath: .spec.mode + name: Mode + type: string + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.attachedEpoch + name: Epoch + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExecutionWorkspace represents one concrete provider-bound environment. + It is controller-created. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExecutionWorkspaceSpec defines one concrete provider-bound + environment. + properties: + attachment: + description: Attachment grants exclusive Task access for Interactive + mode. + properties: + epoch: + description: Epoch monotonically increases for every attachment + attempt. + format: int64 + minimum: 1 + type: integer + expiresAt: + description: ExpiresAt is the hard attachment credential expiry. + format: date-time + type: string + taskRef: + description: TaskRef identifies the attached Task. + properties: + name: + description: Name is the object name. + minLength: 1 + type: string + uid: + description: UID is the immutable object UID. + type: string + required: + - name + - uid + type: object + x-kubernetes-validations: + - message: uid is required + rule: self.uid.size() > 0 + tokenSHA256: + description: TokenSHA256 is the digest of the bearer token held + only in tokenSecretRef. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + tokenSecretRef: + description: TokenSecretRef references the core-owned attachment + Secret in this namespace. + properties: + name: + description: Name is the Secret name in the workspace namespace. + minLength: 1 + type: string + required: + - name + type: object + required: + - epoch + - expiresAt + - taskRef + - tokenSHA256 + - tokenSecretRef + type: object + attachmentEpoch: + description: |- + AttachmentEpoch is the highest attachment epoch allocated by Orka core. + It remains after Attachment is cleared and only increases atomically with + a new attachment intent. + format: int64 + minimum: 1 + type: integer + classBinding: + description: ClassBinding pins the immutable class revision used to + create this workspace. + properties: + generation: + description: Generation is the observed generation used to resolve + the binding. + format: int64 + minimum: 1 + type: integer + name: + description: Name is the bound object's name. + minLength: 1 + type: string + profileHash: + description: |- + ProfileHash is a SHA-256 digest of the functional profile resolved for this binding. + It is populated for class bindings and may be omitted for provider bindings. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + uid: + description: UID is the bound object's immutable Kubernetes UID. + type: string + required: + - generation + - name + - uid + type: object + x-kubernetes-validations: + - message: uid is required + rule: self.uid.size() > 0 + coreAdmission: + description: |- + CoreAdmission is written once by Orka core after validating class and provider policy. + Provider adapters must treat it as read-only and must not progress normal + lifecycle work until this marker matches the immutable bindings, + coreAdmission.admittedGeneration equals metadata.generation, and the + Admitted=True condition records that same generation. The spec marker and + status condition form a dual signal: provider status writers cannot mint the + marker, and ordinary spec writers cannot mint the condition. + properties: + admittedGeneration: + description: AdmittedGeneration is the workspace generation validated + by Orka core. + format: int64 + minimum: 1 + type: integer + classBinding: + description: ClassBinding is the exact class binding admitted + by core. + properties: + generation: + description: Generation is the observed generation used to + resolve the binding. + format: int64 + minimum: 1 + type: integer + name: + description: Name is the bound object's name. + minLength: 1 + type: string + profileHash: + description: |- + ProfileHash is a SHA-256 digest of the functional profile resolved for this binding. + It is populated for class bindings and may be omitted for provider bindings. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + uid: + description: UID is the bound object's immutable Kubernetes + UID. + type: string + required: + - generation + - name + - uid + type: object + x-kubernetes-validations: + - message: uid is required + rule: self.uid.size() > 0 + poolBinding: + description: PoolBinding pins the pool identity used for a pooled + workspace. + properties: + generation: + description: Generation is the observed generation used to + resolve the binding. + format: int64 + minimum: 1 + type: integer + name: + description: Name is the bound object's name. + minLength: 1 + type: string + profileHash: + description: |- + ProfileHash is a SHA-256 digest of the functional profile resolved for this binding. + It is populated for class bindings and may be omitted for provider bindings. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + uid: + description: UID is the bound object's immutable Kubernetes + UID. + type: string + required: + - generation + - name + - uid + type: object + x-kubernetes-validations: + - message: uid is required + rule: self.uid.size() > 0 + providerBinding: + description: ProviderBinding is the exact provider binding admitted + by core. + properties: + generation: + description: Generation is the observed generation used to + resolve the binding. + format: int64 + minimum: 1 + type: integer + name: + description: Name is the bound object's name. + minLength: 1 + type: string + profileHash: + description: |- + ProfileHash is a SHA-256 digest of the functional profile resolved for this binding. + It is populated for class bindings and may be omitted for provider bindings. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + uid: + description: UID is the bound object's immutable Kubernetes + UID. + type: string + required: + - generation + - name + - uid + type: object + x-kubernetes-validations: + - message: uid is required + rule: self.uid.size() > 0 + required: + - admittedGeneration + - classBinding + - providerBinding + type: object + desiredState: + default: Ready + description: DesiredState is owned by Orka core. + enum: + - Ready + - Suspended + - Deleted + - Quarantined + type: string + lifecycle: + description: Lifecycle is the class policy resolved at creation time. + properties: + allowedOnDetach: + description: AllowedOnDetach lists the actions a Task may request. + items: + description: WorkspaceOnDetach is the lifecycle action taken + after an interactive attachment is revoked. + enum: + - Suspend + - Delete + type: string + minItems: 1 + type: array + x-kubernetes-list-type: set + defaultOnDetach: + default: Suspend + description: DefaultOnDetach is used when a Task does not request + an allowed override. + enum: + - Suspend + - Delete + type: string + deletionPolicy: + description: DeletionPolicy describes the required disposition + of provider and retained data. + properties: + checkpoints: + description: Checkpoints controls provider checkpoints and + snapshots. + enum: + - Delete + - Retain + type: string + persistentVolumes: + description: PersistentVolumes controls durable volume retention. + enum: + - Delete + - Retain + type: string + providerResources: + description: ProviderResources controls provider-native compute + and control-plane objects. + enum: + - Delete + - Retain + type: string + required: + - checkpoints + - persistentVolumes + - providerResources + type: object + detachTimeout: + default: 2m + description: DetachTimeout bounds how long Task finalization waits + for attachment revocation. + type: string + idleTimeout: + description: IdleTimeout is the maximum idle duration before the + provider may apply the class policy. + type: string + maxLifetime: + description: MaxLifetime is the maximum lifetime of a concrete + workspace. + type: string + required: + - allowedOnDetach + - defaultOnDetach + - deletionPolicy + - detachTimeout + type: object + x-kubernetes-validations: + - message: defaultOnDetach must be included in allowedOnDetach + rule: self.allowedOnDetach.exists(action, action == self.defaultOnDetach) + - message: detachTimeout must be positive + rule: duration(self.detachTimeout) > duration('0s') + - message: idleTimeout must be positive when set + rule: '!has(self.idleTimeout) || duration(self.idleTimeout) > duration(''0s'')' + - message: maxLifetime must be positive when set + rule: '!has(self.maxLifetime) || duration(self.maxLifetime) > duration(''0s'')' + - message: maxLifetime must be greater than or equal to idleTimeout + rule: '!has(self.idleTimeout) || !has(self.maxLifetime) || duration(self.maxLifetime) + >= duration(self.idleTimeout)' + mode: + description: Mode is copied from the class. + enum: + - Interactive + - Service + type: string + providerBinding: + description: ProviderBinding pins the provider installation used to + create this workspace. + properties: + generation: + description: Generation is the observed generation used to resolve + the binding. + format: int64 + minimum: 1 + type: integer + name: + description: Name is the bound object's name. + minLength: 1 + type: string + profileHash: + description: |- + ProfileHash is a SHA-256 digest of the functional profile resolved for this binding. + It is populated for class bindings and may be omitted for provider bindings. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + uid: + description: UID is the bound object's immutable Kubernetes UID. + type: string + required: + - generation + - name + - uid + type: object + x-kubernetes-validations: + - message: uid is required + rule: self.uid.size() > 0 + service: + description: Service declares requested endpoints for Service mode. + properties: + ports: + description: Ports lists requested service ports. + items: + description: ExecutionWorkspaceServicePort declares a Service-mode + port requested by the owning Tool. + properties: + name: + description: Name is a stable endpoint name. + maxLength: 63 + minLength: 1 + type: string + port: + description: Port is the container/service port. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: Protocol is the endpoint application protocol. + enum: + - HTTP + - HTTPS + - TCP + type: string + required: + - name + - port + - protocol + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - ports + type: object + sessionRef: + description: SessionRef pins the conversation Session for session-scoped + reuse. + properties: + name: + description: Name is the object name. + minLength: 1 + type: string + uid: + description: UID is the immutable object UID. + type: string + required: + - name + - uid + type: object + x-kubernetes-validations: + - message: uid is required + rule: self.uid.size() > 0 + slot: + default: default + description: Slot allows one Session to hold multiple independently + named workspaces. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - classBinding + - desiredState + - lifecycle + - mode + - providerBinding + - slot + type: object + x-kubernetes-validations: + - message: classBinding.profileHash is required + rule: self.classBinding.profileHash.size() > 0 + - message: coreAdmission must match the immutable workspace bindings + rule: '!has(self.coreAdmission) || (self.coreAdmission.classBinding.name + == self.classBinding.name && self.coreAdmission.classBinding.uid == + self.classBinding.uid && self.coreAdmission.classBinding.generation + == self.classBinding.generation && has(self.coreAdmission.classBinding.profileHash) + == has(self.classBinding.profileHash) && (!has(self.coreAdmission.classBinding.profileHash) + || self.coreAdmission.classBinding.profileHash == self.classBinding.profileHash) + && self.coreAdmission.providerBinding.name == self.providerBinding.name + && self.coreAdmission.providerBinding.uid == self.providerBinding.uid + && self.coreAdmission.providerBinding.generation == self.providerBinding.generation + && has(self.coreAdmission.providerBinding.profileHash) == has(self.providerBinding.profileHash) + && (!has(self.coreAdmission.providerBinding.profileHash) || self.coreAdmission.providerBinding.profileHash + == self.providerBinding.profileHash))' + - message: coreAdmission cannot be removed once set + rule: '!has(oldSelf.coreAdmission) || has(self.coreAdmission)' + - message: coreAdmission.admittedGeneration must not decrease + rule: '!has(oldSelf.coreAdmission) || self.coreAdmission.admittedGeneration + >= oldSelf.coreAdmission.admittedGeneration' + - message: mode is immutable + rule: self.mode == oldSelf.mode + - message: classBinding is immutable + rule: self.classBinding == oldSelf.classBinding + - message: providerBinding is immutable + rule: self.providerBinding == oldSelf.providerBinding + - message: sessionRef is immutable + rule: has(self.sessionRef) == has(oldSelf.sessionRef) && (!has(self.sessionRef) + || self.sessionRef == oldSelf.sessionRef) + - message: slot is immutable + rule: self.slot == oldSelf.slot + - message: lifecycle is immutable + rule: self.lifecycle == oldSelf.lifecycle + - message: Deleted desiredState is terminal + rule: oldSelf.desiredState != 'Deleted' || self.desiredState == 'Deleted' + - message: attachmentEpoch must not decrease + rule: '!has(oldSelf.attachmentEpoch) || (has(self.attachmentEpoch) && + self.attachmentEpoch >= oldSelf.attachmentEpoch)' + - message: attachments are only valid for Interactive workspaces + rule: '!has(self.attachment) || self.mode == ''Interactive''' + - message: service ports are only valid for Service workspaces + rule: '!has(self.service) || self.mode == ''Service''' + - message: Service workspaces require service configuration + rule: self.mode != 'Service' || has(self.service) + - message: Interactive workspaces cannot request service ports + rule: self.mode != 'Interactive' || !has(self.service) + status: + description: |- + ExecutionWorkspaceStatus defines adapter-observed state. Exactly one provider adapter owns this status; + Orka core projects it into Task and Tool status rather than allowing adapters to write those resources. + properties: + attachedEpoch: + description: AttachedEpoch is the epoch currently enforced by the + data plane. + format: int64 + type: integer + conditions: + description: Conditions represent admission, provisioning, data-plane, + attachment, and finalization state. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + connectionSecretRef: + description: ConnectionSecretRef contains endpoint, CA, and provider + lifecycle credentials for trusted control-plane consumers. + properties: + name: + description: Name is the Secret name in the workspace namespace. + minLength: 1 + type: string + required: + - name + type: object + disposition: + description: Disposition reports cleanup progress independent of owning + Task/Tool terminal state. + properties: + accessCredentials: + description: ExecutionWorkspaceDispositionState reports progress + for one cleanup category. + enum: + - Pending + - Active + - Revoked + - Retained + - Deleted + - Failed + - NotApplicable + type: string + checkpoints: + description: ExecutionWorkspaceDispositionState reports progress + for one cleanup category. + enum: + - Pending + - Active + - Revoked + - Retained + - Deleted + - Failed + - NotApplicable + type: string + compute: + description: ExecutionWorkspaceDispositionState reports progress + for one cleanup category. + enum: + - Pending + - Active + - Revoked + - Retained + - Deleted + - Failed + - NotApplicable + type: string + ephemeralSecrets: + description: ExecutionWorkspaceDispositionState reports progress + for one cleanup category. + enum: + - Pending + - Active + - Revoked + - Retained + - Deleted + - Failed + - NotApplicable + type: string + persistentVolumes: + description: ExecutionWorkspaceDispositionState reports progress + for one cleanup category. + enum: + - Pending + - Active + - Revoked + - Retained + - Deleted + - Failed + - NotApplicable + type: string + providerResources: + description: ExecutionWorkspaceDispositionState reports progress + for one cleanup category. + enum: + - Pending + - Active + - Revoked + - Retained + - Deleted + - Failed + - NotApplicable + type: string + workspaceData: + description: ExecutionWorkspaceDispositionState reports progress + for one cleanup category. + enum: + - Pending + - Active + - Revoked + - Retained + - Deleted + - Failed + - NotApplicable + type: string + required: + - accessCredentials + - checkpoints + - compute + - ephemeralSecrets + - persistentVolumes + - providerResources + - workspaceData + type: object + endpoints: + description: Endpoints contains sanitized endpoint metadata only. + items: + description: |- + ExecutionWorkspaceEndpoint is sanitized endpoint metadata. Credentials and private connection material + are always delivered through a Secret and never placed in status. + properties: + name: + description: Name matches a requested service port or the reserved + data-plane name. + type: string + protocol: + description: Protocol is the endpoint application protocol. + type: string + url: + description: URL is the sanitized endpoint URL. It must not + contain userinfo or credentials. + minLength: 1 + pattern: ^(https?|tcp)://[^/:@?#[:space:]][^@?#[:space:]]*$ + type: string + required: + - name + - url + type: object + type: array + externalID: + description: ExternalID is a sanitized opaque provider resource identifier + for operator diagnostics. + type: string + observedGeneration: + description: ObservedGeneration is the most recent spec generation + observed by the adapter. + format: int64 + type: integer + providerBinding: + description: ProviderBinding records the exact adapter/backend contract + serving this workspace. + properties: + adapterDigest: + description: AdapterDigest is the immutable adapter build digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + adapterVersion: + description: AdapterVersion is the adapter semantic version. + type: string + backendAPIVersion: + description: BackendAPIVersion is the selected provider-native + API version. + type: string + contractVersion: + description: ContractVersion is the selected generic provider + contract. + type: string + type: object + state: + description: State is the provider-neutral lifecycle state. + enum: + - Pending + - Provisioning + - Ready + - Attaching + - Attached + - Detaching + - Suspending + - Suspended + - Deleting + - Deleted + - Quarantined + - Failed + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/executionworkspaceclass-customresourcedefinition.yaml b/charts/orka/crds/executionworkspaceclass-customresourcedefinition.yaml new file mode 100644 index 000000000..0b8f5632c --- /dev/null +++ b/charts/orka/crds/executionworkspaceclass-customresourcedefinition.yaml @@ -0,0 +1,333 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: executionworkspaceclasses.workspace.orka.ai +spec: + group: workspace.orka.ai + names: + categories: + - orka + kind: ExecutionWorkspaceClass + listKind: ExecutionWorkspaceClassList + plural: executionworkspaceclasses + shortNames: + - ewc + singular: executionworkspaceclass + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.mode + name: Mode + type: string + - jsonPath: .status.providerRef.name + name: Provider + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExecutionWorkspaceClass represents an immutable user-selectable + workspace profile and policy. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ExecutionWorkspaceClassSpec defines a user-selectable environment and policy. + Direct provisioning requires both providerRef and parametersRef; pooled provisioning requires poolRef. + properties: + allowedReuseScopes: + description: AllowedReuseScopes lists the reuse scopes Tasks may request. + items: + description: WorkspaceReuseScope is a reuse scope a class permits. + enum: + - None + - Session + type: string + minItems: 1 + type: array + x-kubernetes-list-type: set + lifecycle: + description: Lifecycle defines detach, retention, and deletion policy. + properties: + allowedOnDetach: + description: AllowedOnDetach lists the actions a Task may request. + items: + description: WorkspaceOnDetach is the lifecycle action taken + after an interactive attachment is revoked. + enum: + - Suspend + - Delete + type: string + minItems: 1 + type: array + x-kubernetes-list-type: set + defaultOnDetach: + default: Suspend + description: DefaultOnDetach is used when a Task does not request + an allowed override. + enum: + - Suspend + - Delete + type: string + deletionPolicy: + description: DeletionPolicy describes the required disposition + of provider and retained data. + properties: + checkpoints: + description: Checkpoints controls provider checkpoints and + snapshots. + enum: + - Delete + - Retain + type: string + persistentVolumes: + description: PersistentVolumes controls durable volume retention. + enum: + - Delete + - Retain + type: string + providerResources: + description: ProviderResources controls provider-native compute + and control-plane objects. + enum: + - Delete + - Retain + type: string + required: + - checkpoints + - persistentVolumes + - providerResources + type: object + detachTimeout: + default: 2m + description: DetachTimeout bounds how long Task finalization waits + for attachment revocation. + type: string + idleTimeout: + description: IdleTimeout is the maximum idle duration before the + provider may apply the class policy. + type: string + maxLifetime: + description: MaxLifetime is the maximum lifetime of a concrete + workspace. + type: string + required: + - allowedOnDetach + - defaultOnDetach + - deletionPolicy + - detachTimeout + type: object + x-kubernetes-validations: + - message: defaultOnDetach must be included in allowedOnDetach + rule: self.allowedOnDetach.exists(action, action == self.defaultOnDetach) + - message: detachTimeout must be positive + rule: duration(self.detachTimeout) > duration('0s') + - message: idleTimeout must be positive when set + rule: '!has(self.idleTimeout) || duration(self.idleTimeout) > duration(''0s'')' + - message: maxLifetime must be positive when set + rule: '!has(self.maxLifetime) || duration(self.maxLifetime) > duration(''0s'')' + - message: maxLifetime must be greater than or equal to idleTimeout + rule: '!has(self.idleTimeout) || !has(self.maxLifetime) || duration(self.maxLifetime) + >= duration(self.idleTimeout)' + mode: + default: Interactive + description: Mode selects interactive Task attachment or persistent + Service hosting. + enum: + - Interactive + - Service + type: string + parametersRef: + description: ParametersRef points at namespaced adapter-owned workspace + profile parameters. + properties: + group: + description: Group is the API group of the referenced object. + minLength: 1 + type: string + kind: + description: Kind is the kind of the referenced object. + minLength: 1 + type: string + name: + description: Name is the name of the referenced object. + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + poolRef: + description: PoolRef selects a pool in the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + providerRef: + description: ProviderRef selects a cluster-scoped provider for direct + provisioning. + properties: + name: + description: Name is the name of the referenced cluster-scoped + object. + minLength: 1 + type: string + required: + - name + type: object + requiredFeatures: + description: RequiredFeatures must be a subset of the selected provider's + advertised features. + items: + description: ExecutionWorkspaceFeature names a provider capability + required by a class. + pattern: ^[a-z][a-z0-9.-]{0,62}$ + type: string + type: array + x-kubernetes-list-type: set + required: + - allowedReuseScopes + - lifecycle + - mode + type: object + x-kubernetes-validations: + - message: 'exactly one provisioning source is required: providerRef with + parametersRef, or poolRef' + rule: (has(self.providerRef) && has(self.parametersRef) && !has(self.poolRef)) + || (!has(self.providerRef) && !has(self.parametersRef) && has(self.poolRef)) + - message: poolRef.name is required + rule: '!has(self.poolRef) || self.poolRef.name.size() > 0' + - message: ExecutionWorkspaceClass functional spec is immutable; create + a new class + rule: self == oldSelf + - message: Service classes may only allow the None reuse scope + rule: self.mode == 'Interactive' || self.allowedReuseScopes.all(scope, + scope == 'None') + status: + description: ExecutionWorkspaceClassStatus reports resolution and readiness + without provider-native details. + properties: + conditions: + description: Conditions represent reference, feature, and policy readiness. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: ObservedGeneration is the most recent generation observed + by core. + format: int64 + type: integer + profileHash: + description: |- + ProfileHash pins the first successfully resolved functional class profile. + Later referenced-object drift makes the class NotReady instead of changing this hash. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + providerRef: + description: ProviderRef is the provider resolved directly or through + the selected pool. + properties: + name: + description: Name is the name of the referenced cluster-scoped + object. + minLength: 1 + type: string + required: + - name + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/executionworkspacepool-customresourcedefinition.yaml b/charts/orka/crds/executionworkspacepool-customresourcedefinition.yaml new file mode 100644 index 000000000..66ab111ff --- /dev/null +++ b/charts/orka/crds/executionworkspacepool-customresourcedefinition.yaml @@ -0,0 +1,224 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: executionworkspacepools.workspace.orka.ai +spec: + group: workspace.orka.ai + names: + categories: + - orka + kind: ExecutionWorkspacePool + listKind: ExecutionWorkspacePoolList + plural: executionworkspacepools + shortNames: + - ewpool + singular: executionworkspacepool + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.providerRef.name + name: Provider + type: string + - jsonPath: .status.available + name: Available + type: integer + - jsonPath: .status.allocated + name: Allocated + type: integer + - jsonPath: .status.total + name: Total + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExecutionWorkspacePool represents provider-managed warm or reusable + capacity. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExecutionWorkspacePoolSpec defines generic provider-managed + capacity. + properties: + capacity: + description: Capacity is the desired provider-managed capacity envelope. + properties: + maxSize: + description: MaxSize is the hard provider-side allocation limit + for this pool. + format: int32 + minimum: 0 + type: integer + minReady: + description: MinReady is the desired minimum number of immediately + allocatable workspaces. + format: int32 + minimum: 0 + type: integer + required: + - maxSize + - minReady + type: object + x-kubernetes-validations: + - message: maxSize must be greater than or equal to minReady + rule: self.maxSize >= self.minReady + parametersRef: + description: ParametersRef points at namespaced adapter-owned pool + parameters in this namespace. + properties: + group: + description: Group is the API group of the referenced object. + minLength: 1 + type: string + kind: + description: Kind is the kind of the referenced object. + minLength: 1 + type: string + name: + description: Name is the name of the referenced object. + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + providerRef: + description: ProviderRef selects the cluster-scoped provider installation. + properties: + name: + description: Name is the name of the referenced cluster-scoped + object. + minLength: 1 + type: string + required: + - name + type: object + required: + - capacity + - parametersRef + - providerRef + type: object + x-kubernetes-validations: + - message: providerRef is immutable + rule: self.providerRef == oldSelf.providerRef + - message: parametersRef is immutable + rule: self.parametersRef == oldSelf.parametersRef + status: + description: ExecutionWorkspacePoolStatus reports provider-independent + pool counts. + properties: + allocated: + description: Allocated is non-suspended capacity currently reserved + for or bound to concrete workspaces. + format: int32 + type: integer + available: + description: Available is immediately allocatable capacity. + format: int32 + type: integer + conditions: + description: Conditions represent readiness, admission, draining, + and capacity pressure. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: ObservedGeneration is the most recent generation observed + by the adapter. + format: int64 + type: integer + suspended: + description: Suspended is a disjoint bucket of reusable capacity not + consuming active compute. + format: int32 + type: integer + total: + description: Total is all capacity represented by the disjoint Available, + Allocated, and Suspended buckets. + format: int32 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/executionworkspaceprovider-customresourcedefinition.yaml b/charts/orka/crds/executionworkspaceprovider-customresourcedefinition.yaml new file mode 100644 index 000000000..f7e4347d3 --- /dev/null +++ b/charts/orka/crds/executionworkspaceprovider-customresourcedefinition.yaml @@ -0,0 +1,292 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: executionworkspaceproviders.workspace.orka.ai +spec: + group: workspace.orka.ai + names: + categories: + - orka + kind: ExecutionWorkspaceProvider + listKind: ExecutionWorkspaceProviderList + plural: executionworkspaceproviders + shortNames: + - ewp + singular: executionworkspaceprovider + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.lifecycleState + name: State + type: string + - jsonPath: .spec.controllerName + name: Controller + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExecutionWorkspaceProvider is a cluster-scoped provider adapter + installation. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExecutionWorkspaceProviderSpec defines a configured provider + adapter installation. + properties: + controllerName: + description: ControllerName is the globally unique adapter controller + identity. + maxLength: 253 + minLength: 1 + type: string + lifecycleState: + default: Active + description: LifecycleState controls new allocations while preserving + cleanup for existing workspaces. + enum: + - Active + - Draining + - Disabled + type: string + parametersRef: + description: ParametersRef points at the adapter-owned provider configuration + object. + properties: + group: + description: Group is the API group of the referenced object. + minLength: 1 + type: string + kind: + description: Kind is the kind of the referenced object. + minLength: 1 + type: string + name: + description: Name is the name of the referenced object. + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requiredContracts: + description: RequiredContracts lists contracts the adapter must advertise + before this provider is usable. + items: + type: string + minItems: 1 + type: array + x-kubernetes-list-type: set + usagePolicy: + description: UsagePolicy constrains which namespaces may resolve classes + to this provider. + properties: + allowedNamespaceSelector: + description: |- + AllowedNamespaceSelector selects namespaces that may resolve classes to this provider. + An empty selector matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + required: + - controllerName + - lifecycleState + - parametersRef + - requiredContracts + type: object + x-kubernetes-validations: + - message: controllerName is immutable + rule: self.controllerName == oldSelf.controllerName + - message: parametersRef is immutable + rule: self.parametersRef == oldSelf.parametersRef + - message: requiredContracts is immutable + rule: self.requiredContracts == oldSelf.requiredContracts + status: + description: |- + ExecutionWorkspaceProviderStatus defines the observed provider state. The matching adapter is the + sole writer of adapter/backend/features/heartbeat; Orka core owns only generic usability conditions. + properties: + adapter: + description: Adapter reports adapter build identity. + properties: + digest: + description: Digest is the immutable adapter image or build digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + version: + description: Version is the adapter semantic version. + type: string + type: object + backend: + description: Backend reports safe provider-native compatibility metadata. + properties: + apiVersions: + description: APIVersions lists provider-native API versions understood + by the adapter. + items: + type: string + type: array + version: + description: Version is the observed backend version. + type: string + type: object + conditions: + description: Conditions represent generic readiness and compatibility. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastHeartbeat: + description: LastHeartbeat is the last successful adapter heartbeat. + format: date-time + type: string + observedGeneration: + description: ObservedGeneration is the most recent spec generation + observed by the adapter. + format: int64 + type: integer + supportedContracts: + description: SupportedContracts lists generic control/data-plane contracts + implemented by this adapter. + items: + type: string + type: array + x-kubernetes-list-type: set + supportedFeatures: + description: SupportedFeatures lists generic features implemented + by this installation. + items: + description: ExecutionWorkspaceFeature names a provider capability + required by a class. + pattern: ^[a-z][a-z0-9.-]{0,62}$ + type: string + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/externaleffect-customresourcedefinition.yaml b/charts/orka/crds/externaleffect-customresourcedefinition.yaml new file mode 100644 index 000000000..e17b0a44f --- /dev/null +++ b/charts/orka/crds/externaleffect-customresourcedefinition.yaml @@ -0,0 +1,191 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: externaleffects.core.orka.ai +spec: + group: core.orka.ai + names: + kind: ExternalEffect + listKind: ExternalEffectList + plural: externaleffects + shortNames: + - eeffect + singular: externaleffect + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .spec.kind + name: Kind + type: string + - jsonPath: .status.attempts + name: Attempts + type: integer + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ExternalEffect is the Kubernetes-authoritative canonical idempotency record + for an operation outside SQLite. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ExternalEffectSpec is the immutable canonical identity and request binding. + The identity namespace intentionally duplicates metadata.namespace so a + serialized record remains self-describing and can be checked fail-closed. + properties: + aggregateId: + maxLength: 1024 + minLength: 1 + type: string + id: + maxLength: 1024 + minLength: 1 + type: string + identityNamespace: + maxLength: 1024 + minLength: 1 + type: string + kind: + maxLength: 1024 + minLength: 1 + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + required: + - aggregateId + - id + - identityNamespace + - kind + - operationId + - requestDigest + type: object + x-kubernetes-validations: + - message: external effect spec is immutable + rule: self == oldSelf + status: + description: |- + ExternalEffectStatus contains the mutable state, response, lease, and epoch + fence for one canonical external effect. + properties: + attempts: + format: int64 + minimum: 0 + type: integer + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + leaseExpiresAt: + format: date-time + type: string + leaseOwner: + maxLength: 1024 + type: string + response: + description: |- + Response stores a bounded JSON response for idempotent replay. Large + response bodies should remain in the artifact store and be referenced by + a compact receipt instead. + x-kubernetes-preserve-unknown-fields: true + responseDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + state: + description: |- + ExternalEffectControlState is the durable state of one idempotent operation + performed outside the controller's SQLite transaction boundary. + enum: + - Pending + - InFlight + - Succeeded + - Failed + - OutcomeUnknown + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: in-flight external effects require a lease owner and expiry + rule: '!has(self.state) || self.state != ''InFlight'' || (has(self.leaseOwner) + && size(self.leaseOwner) > 0 && has(self.leaseExpiresAt))' + - message: non-in-flight external effects must clear lease fields + rule: '!has(self.state) || self.state == ''InFlight'' || ((!has(self.leaseOwner) + || size(self.leaseOwner) == 0) && !has(self.leaseExpiresAt))' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/fakepoolparameters-customresourcedefinition.yaml b/charts/orka/crds/fakepoolparameters-customresourcedefinition.yaml new file mode 100644 index 000000000..3aaba45cc --- /dev/null +++ b/charts/orka/crds/fakepoolparameters-customresourcedefinition.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: fakepoolparameters.fake.workspace.orka.ai +spec: + group: fake.workspace.orka.ai + names: + categories: + - orka + kind: FakePoolParameters + listKind: FakePoolParametersList + plural: fakepoolparameters + singular: fakepoolparameters + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + FakePoolParameters is the namespaced parameters identity used by the + development-only fake workspace pool reconciler. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + FakePoolParametersSpec is intentionally empty. Pool capacity is expressed by + the generic ExecutionWorkspacePool resource; this object provides an + adapter-owned namespaced parameters identity for conformance. + type: object + type: object + served: true + storage: true diff --git a/charts/orka/crds/fakeproviderconfig-customresourcedefinition.yaml b/charts/orka/crds/fakeproviderconfig-customresourcedefinition.yaml new file mode 100644 index 000000000..bc16c2a46 --- /dev/null +++ b/charts/orka/crds/fakeproviderconfig-customresourcedefinition.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: fakeproviderconfigs.fake.workspace.orka.ai +spec: + group: fake.workspace.orka.ai + names: + categories: + - orka + kind: FakeProviderConfig + listKind: FakeProviderConfigList + plural: fakeproviderconfigs + singular: fakeproviderconfig + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + FakeProviderConfig is the cluster-scoped configuration identity used by the + development-only fake workspace provider. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + FakeProviderConfigSpec is intentionally empty. The in-memory development + provider needs only a concrete cluster-scoped identity for reference and + REST-mapping conformance. + type: object + type: object + served: true + storage: true diff --git a/charts/orka/crds/outboundaccesspolicy-customresourcedefinition.yaml b/charts/orka/crds/outboundaccesspolicy-customresourcedefinition.yaml new file mode 100644 index 000000000..e51faf755 --- /dev/null +++ b/charts/orka/crds/outboundaccesspolicy-customresourcedefinition.yaml @@ -0,0 +1,549 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: outboundaccesspolicies.core.orka.ai +spec: + group: core.orka.ai + names: + kind: OutboundAccessPolicy + listKind: OutboundAccessPolicyList + plural: outboundaccesspolicies + singular: outboundaccesspolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="ResolvedRefs")].status + name: ResolvedRefs + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + OutboundAccessPolicy governs resource credentials or trusted gateway routing + for Tools in the same namespace. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + OutboundAccessPolicySpec configures exactly one outbound credential or + routing adapter. + properties: + direct: + description: Direct exchanges a resolved subject for a downstream + resource credential. + properties: + actor: + description: Actor optionally resolves an RFC 8693 actor token. + properties: + secretRef: + description: SecretRef selects a same-namespace Secret value. + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + serviceAccountRef: + description: ServiceAccountRef selects a ServiceAccount and + TokenRequest parameters. + properties: + audiences: + description: Audiences are requested for the projected + ServiceAccount token. + items: + type: string + type: array + expirationSeconds: + default: 600 + description: ExpirationSeconds requests a bounded token + lifetime. + format: int64 + maximum: 3600 + minimum: 600 + type: integer + name: + description: Name is the exact same-namespace ServiceAccount + name. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + source: + description: Source selects TransactionToken, ServiceAccount, + or SecretRef. + enum: + - TransactionToken + - ServiceAccount + - SecretRef + type: string + tokenType: + description: |- + TokenType is an arbitrary RFC 8693 token-type URN. It is not used by + RFC 7523 JWT bearer grants. + type: string + required: + - source + type: object + x-kubernetes-validations: + - message: TransactionToken must not configure Secret or ServiceAccount + references + rule: self.source != 'TransactionToken' || (!has(self.secretRef) + && !has(self.serviceAccountRef)) + - message: ServiceAccount requires only serviceAccountRef + rule: self.source != 'ServiceAccount' || (has(self.serviceAccountRef) + && !has(self.secretRef)) + - message: SecretRef requires only secretRef + rule: self.source != 'SecretRef' || (has(self.secretRef) && + !has(self.serviceAccountRef)) + additionalParameters: + additionalProperties: + type: string + description: AdditionalParameters are static form parameters. + Reserved OAuth fields are rejected. + maxProperties: 32 + type: object + audiences: + description: Audiences are emitted as repeated OAuth audience + parameters. + items: + type: string + type: array + clientAuthentication: + description: ClientAuthentication configures OAuth client authentication. + properties: + audience: + description: Audience overrides the private_key_jwt aud claim. + The endpoint is the default. + type: string + clientID: + description: ClientID is required by confidential-client methods. + type: string + clientSecretRef: + description: ClientSecretRef supplies client-secret basic/post + credentials. + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + keyID: + description: KeyID is copied into the private_key_jwt header. + type: string + method: + default: None + description: Method defaults to None. + enum: + - None + - ClientSecretBasic + - ClientSecretPost + - PrivateKeyJWT + type: string + privateKeyRef: + description: PrivateKeyRef supplies a PEM RSA or P-256 key + for private_key_jwt. + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + type: object + x-kubernetes-validations: + - message: None must not configure client credentials + rule: self.method != 'None' || (!has(self.clientSecretRef) && + !has(self.privateKeyRef)) + - message: client-secret methods require clientID and clientSecretRef + only + rule: '!(self.method in [''ClientSecretBasic'',''ClientSecretPost'']) + || (has(self.clientID) && self.clientID.size() > 0 && has(self.clientSecretRef) + && !has(self.privateKeyRef))' + - message: PrivateKeyJWT requires clientID and privateKeyRef only + rule: self.method != 'PrivateKeyJWT' || (has(self.clientID) + && self.clientID.size() > 0 && has(self.privateKeyRef) && + !has(self.clientSecretRef)) + expectedIssuedTokenType: + description: |- + ExpectedIssuedTokenType is the exact issued_token_type required for RFC 8693. + RFC 7523 responses may omit issued_token_type. + minLength: 1 + type: string + grant: + default: TokenExchange + description: Grant selects RFC 8693 token exchange or RFC 7523 + JWT bearer. + enum: + - TokenExchange + - JWTBearer + type: string + output: + description: Output configures resource credential injection. + properties: + header: + default: Authorization + description: Header defaults to Authorization. Txn-Token is + forbidden. + type: string + prefix: + default: 'Bearer ' + description: Prefix defaults to "Bearer ". Set an explicit + empty string for no prefix. + type: string + type: object + x-kubernetes-validations: + - message: Txn-Token cannot be used as the resource credential + output header + rule: '!has(self.header) || self.header.lowerAscii() != ''txn-token''' + requestedTokenType: + description: RequestedTokenType is an arbitrary OAuth token-type + URN. + type: string + resources: + description: Resources are emitted as repeated RFC 8707 resource + parameters. + items: + type: string + type: array + scopes: + description: |- + Scopes are emitted as a space-separated OAuth scope parameter. + TransactionToken subjects may request only a subset of the parent scope. + items: + type: string + type: array + subject: + description: Subject resolves the subject token or JWT assertion. + properties: + secretRef: + description: SecretRef selects a same-namespace Secret value. + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + serviceAccountRef: + description: ServiceAccountRef selects a ServiceAccount and + TokenRequest parameters. + properties: + audiences: + description: Audiences are requested for the projected + ServiceAccount token. + items: + type: string + type: array + expirationSeconds: + default: 600 + description: ExpirationSeconds requests a bounded token + lifetime. + format: int64 + maximum: 3600 + minimum: 600 + type: integer + name: + description: Name is the exact same-namespace ServiceAccount + name. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + source: + description: Source selects TransactionToken, ServiceAccount, + or SecretRef. + enum: + - TransactionToken + - ServiceAccount + - SecretRef + type: string + tokenType: + description: |- + TokenType is an arbitrary RFC 8693 token-type URN. It is not used by + RFC 7523 JWT bearer grants. + type: string + required: + - source + type: object + x-kubernetes-validations: + - message: TransactionToken must not configure Secret or ServiceAccount + references + rule: self.source != 'TransactionToken' || (!has(self.secretRef) + && !has(self.serviceAccountRef)) + - message: ServiceAccount requires only serviceAccountRef + rule: self.source != 'ServiceAccount' || (has(self.serviceAccountRef) + && !has(self.secretRef)) + - message: SecretRef requires only secretRef + rule: self.source != 'SecretRef' || (has(self.secretRef) && + !has(self.serviceAccountRef)) + tokenEndpoint: + description: TokenEndpoint is the exact OAuth token endpoint. + properties: + path: + description: Path is the exact endpoint path used with ServiceRef. + type: string + scheme: + description: Scheme is used with ServiceRef and defaults to + https. + enum: + - http + - https + type: string + serviceRef: + description: |- + ServiceRef selects a token endpoint Service. Cross-namespace refs require + an exact controller allowlist entry. + properties: + name: + type: string + namespace: + description: |- + Namespace defaults to the policy namespace. Cross-namespace refs require + an exact trusted Service-reference allowlist entry. + type: string + port: + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + - port + type: object + tls: + description: TLS configures verification for an HTTPS endpoint. + properties: + caSecretRef: + description: CASecretRef selects PEM CA data from a same-namespace + Secret. + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + serverName: + description: ServerName overrides TLS SNI and hostname + verification. + type: string + type: object + url: + description: URL is an exact absolute HTTPS token endpoint + without userinfo. + type: string + type: object + x-kubernetes-validations: + - message: exactly one of url or serviceRef is required + rule: has(self.url) != has(self.serviceRef) + - message: scheme and path are only valid with serviceRef + rule: '!has(self.url) || (!has(self.scheme) && !has(self.path))' + - message: url token endpoints must use https + rule: '!has(self.url) || self.url.startsWith(''https://'')' + required: + - subject + - tokenEndpoint + type: object + x-kubernetes-validations: + - message: JWTBearer does not support actor + rule: self.grant != 'JWTBearer' || !has(self.actor) + - message: JWTBearer subject must not set tokenType + rule: self.grant != 'JWTBearer' || !has(self.subject.tokenType) + - message: SecretRef token exchange subjects require tokenType + rule: self.grant != 'TokenExchange' || self.subject.source != 'SecretRef' + || (has(self.subject.tokenType) && self.subject.tokenType.size() + > 0) + - message: SecretRef token exchange actors require tokenType + rule: self.grant != 'TokenExchange' || !has(self.actor) || self.actor.source + != 'SecretRef' || (has(self.actor.tokenType) && self.actor.tokenType.size() + > 0) + - message: TokenExchange requires expectedIssuedTokenType + rule: self.grant != 'TokenExchange' || (has(self.expectedIssuedTokenType) + && self.expectedIssuedTokenType.size() > 0) + - message: additionalParameters must not contain reserved OAuth fields + rule: '!has(self.additionalParameters) || self.additionalParameters.all(k, + !(k.lowerAscii() in [''grant_type'',''subject_token'',''subject_token_type'',''actor_token'',''actor_token_type'',''assertion'',''scope'',''audience'',''resource'',''requested_token_type'',''client_id'',''client_secret'',''client_assertion'',''client_assertion_type'']))' + gateway: + description: Gateway routes the original Tool request through a trusted + Kubernetes Service. + properties: + scheme: + default: http + description: Scheme defaults to http for in-cluster gateways. + enum: + - http + - https + type: string + serviceRef: + description: ServiceRef selects the gateway Service. + properties: + name: + type: string + namespace: + description: |- + Namespace defaults to the policy namespace. Cross-namespace refs require + an exact trusted Service-reference allowlist entry. + type: string + port: + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + - port + type: object + tls: + description: TLS configures gateway verification when scheme is + https. + properties: + caSecretRef: + description: CASecretRef selects PEM CA data from a same-namespace + Secret. + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + serverName: + description: ServerName overrides TLS SNI and hostname verification. + type: string + type: object + required: + - serviceRef + type: object + type: object + x-kubernetes-validations: + - message: exactly one of direct or gateway is required + rule: has(self.direct) != has(self.gateway) + status: + description: OutboundAccessPolicyStatus contains only safe validation + state. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/promptattempt-customresourcedefinition.yaml b/charts/orka/crds/promptattempt-customresourcedefinition.yaml new file mode 100644 index 000000000..e9bdef997 --- /dev/null +++ b/charts/orka/crds/promptattempt-customresourcedefinition.yaml @@ -0,0 +1,264 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: promptattempts.core.orka.ai +spec: + group: core.orka.ai + names: + kind: PromptAttempt + listKind: PromptAttemptList + plural: promptattempts + shortNames: + - pattempt + singular: promptattempt + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.executionState + name: Execution + type: string + - jsonPath: .status.deliveryState + name: Delivery + type: string + - jsonPath: .spec.attempt + name: Attempt + type: integer + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PromptAttempt is the Kubernetes-authoritative prompt execution and delivery + control record. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PromptAttemptSpec is the immutable identity and request binding for one + Task prompt attempt. + properties: + attempt: + description: Attempt is the one-based Task attempt number. + format: int64 + minimum: 1 + type: integer + credentialBindings: + description: CredentialBindings is the immutable, role-separated Secret + identity set. + items: + description: |- + PromptCredentialBinding freezes one role-specific Secret identity without + storing credential material. + properties: + namespace: + maxLength: 253 + minLength: 1 + type: string + resourceVersion: + maxLength: 253 + minLength: 1 + type: string + role: + enum: + - SourceRead + - TargetRead + - TargetWrite + - Forge + type: string + secretKey: + maxLength: 253 + minLength: 1 + type: string + secretName: + maxLength: 253 + minLength: 1 + type: string + secretUid: + maxLength: 253 + minLength: 1 + type: string + required: + - namespace + - resourceVersion + - role + - secretKey + - secretName + - secretUid + type: object + maxItems: 4 + type: array + x-kubernetes-list-map-keys: + - role + x-kubernetes-list-type: map + id: + description: ID is the canonical DurableControlStore prompt-attempt + ID. + maxLength: 1024 + minLength: 1 + type: string + promptId: + description: PromptID is the immutable prompt identity within the + attempt. + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + description: RequestDigest binds the prompt identity to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + taskUid: + description: TaskUID is the immutable Kubernetes UID of the Task. + maxLength: 1024 + minLength: 1 + type: string + required: + - attempt + - id + - promptId + - requestDigest + - taskUid + type: object + x-kubernetes-validations: + - message: prompt attempt spec is immutable + rule: self == oldSelf + status: + description: PromptAttemptStatus holds the exact execution and delivery + state machines. + properties: + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + deliveryState: + description: PromptAttemptDeliveryState is the durable delivery state + for one prompt. + enum: + - NotRequested + - Validating + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - ReadValidated + - NoChange + - CancelledBeforePublish + - ReadOnlyWorkspaceModified + - DeliveryConflict + - CredentialBlocked + - PublicationOutcomeUnknown + type: string + executionState: + description: PromptAttemptExecutionState is the durable prompt execution + state. + enum: + - Queued + - Reserved + - SessionStarting + - Planned + - Submitting + - SubmittedUnknown + - Accepted + - Running + - Settling + - Succeeded + - Failed + - Cancelled + - OutcomeUnknown + type: string + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + outcomeMarker: + maxLength: 16384 + type: string + runtimeInstanceId: + description: RuntimeInstanceID is immutable after first binding. + maxLength: 1024 + type: string + sessionLeaseGeneration: + description: SessionLeaseGeneration is immutable after first binding. + format: int64 + minimum: 1 + type: integer + sessionUid: + description: SessionUID is immutable after first binding. + maxLength: 1024 + type: string + terminalReason: + maxLength: 16384 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: OutcomeUnknown requires an explicit outcome marker + rule: '!has(self.executionState) || self.executionState != ''OutcomeUnknown'' + || (has(self.outcomeMarker) && size(self.outcomeMarker) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/publication-customresourcedefinition.yaml b/charts/orka/crds/publication-customresourcedefinition.yaml new file mode 100644 index 000000000..19fc60f75 --- /dev/null +++ b/charts/orka/crds/publication-customresourcedefinition.yaml @@ -0,0 +1,523 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: publications.core.orka.ai +spec: + group: core.orka.ai + names: + kind: Publication + listKind: PublicationList + plural: publications + shortNames: + - pubctl + singular: publication + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .spec.generation + name: Generation + type: integer + - jsonPath: .spec.targetRef + name: Target + type: string + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Publication is the Kubernetes-authoritative clean-room publication + record. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PublicationSpec is the immutable clean-room publication identity and input. + Mutable receipts and forge intent live only in status. + properties: + artifactDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + artifactId: + description: ArtifactID identifies the durable content-addressed change + artifact. + maxLength: 1024 + minLength: 1 + type: string + artifactMediaType: + maxLength: 255 + minLength: 1 + type: string + artifactSizeBytes: + format: int64 + minimum: 1 + type: integer + attempt: + format: int64 + minimum: 1 + type: integer + baseline: + description: |- + ControlRemoteRefState is an exact remote-ref observation. Absent and SHA are + mutually exclusive. The all-zero value is reserved for an explicitly unknown + observation in PublicationOutcomeUnknown receipts. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + branchClaimGeneration: + format: int64 + minimum: 1 + type: integer + branchClaimId: + maxLength: 1024 + minLength: 1 + type: string + commitIdentity: + maxLength: 1024 + minLength: 1 + type: string + commitMessage: + maxLength: 16384 + minLength: 1 + type: string + commitTimestamp: + format: date-time + type: string + generation: + format: int64 + minimum: 1 + type: integer + id: + maxLength: 1024 + minLength: 1 + type: string + promptId: + maxLength: 1024 + minLength: 1 + type: string + publicationCredentialRef: + description: |- + PublicationCredentialRef identifies an operation-scoped Secret reference; + it never contains credential material. + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + sessionUid: + maxLength: 1024 + type: string + sourceBaselineSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + sourceRef: + description: SourceRef is the exact immutable source ref or revision + selector. + maxLength: 1024 + minLength: 1 + type: string + sourceRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + targetRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + targetRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + taskUid: + maxLength: 1024 + minLength: 1 + type: string + required: + - artifactDigest + - artifactId + - artifactMediaType + - artifactSizeBytes + - attempt + - baseline + - branchClaimGeneration + - branchClaimId + - commitIdentity + - commitMessage + - commitTimestamp + - generation + - id + - promptId + - publicationCredentialRef + - requestDigest + - sourceBaselineSha + - sourceRef + - sourceRepositoryId + - targetRef + - targetRepositoryId + - taskUid + type: object + x-kubernetes-validations: + - message: publication spec is immutable + rule: self == oldSelf + status: + description: PublicationStatus contains mutable state, exact receipts, + and epoch fencing. + properties: + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + prIntent: + description: |- + PublicationPullRequestIntent is the exact forge tuple persisted before the + first forge API call. + properties: + baseRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + baseRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + expectedHeadSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + headRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + headRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + publicationGeneration: + format: int64 + minimum: 1 + type: integer + required: + - baseRef + - baseRepositoryId + - expectedHeadSha + - headRef + - headRepositoryId + - publicationGeneration + type: object + preparedReceipt: + description: PreparedPublicationControlReceipt records deterministic + commit preparation. + properties: + bundleArtifactId: + maxLength: 1024 + minLength: 1 + type: string + bundleDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + bundleMediaType: + maxLength: 255 + minLength: 1 + type: string + bundleRef: + pattern: ^refs/orka/publications/[a-f0-9]{64}$ + type: string + bundleSizeBytes: + format: int64 + minimum: 1 + type: integer + commitSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + manifestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + preparedAt: + format: date-time + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + treeSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - bundleArtifactId + - bundleDigest + - bundleMediaType + - bundleRef + - bundleSizeBytes + - commitSha + - manifestDigest + - operationId + - preparedAt + - requestDigest + - treeSha + type: object + publishReceipt: + description: PublishOperationControlReceipt records the exact server-enforced + ref CAS. + properties: + acknowledgementUnknown: + type: boolean + expectedCommitSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + publishedAt: + format: date-time + type: string + remoteBefore: + description: |- + ControlRemoteRefState is an exact remote-ref observation. Absent and SHA are + mutually exclusive. The all-zero value is reserved for an explicitly unknown + observation in PublicationOutcomeUnknown receipts. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + targetRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + targetRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + required: + - acknowledgementUnknown + - expectedCommitSha + - operationId + - publishedAt + - remoteBefore + - requestDigest + - targetRef + - targetRepositoryId + type: object + pullRequestReceipt: + description: PullRequestOperationControlReceipt snapshots exact forge + reconciliation. + properties: + forgeId: + maxLength: 1024 + minLength: 1 + type: string + headSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + intentKey: + maxLength: 1024 + minLength: 1 + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + reconciledAt: + format: date-time + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + state: + maxLength: 128 + minLength: 1 + type: string + url: + maxLength: 2048 + minLength: 1 + type: string + required: + - forgeId + - headSha + - intentKey + - operationId + - reconciledAt + - requestDigest + - state + - url + type: object + state: + description: PublicationControlState is the clean-room publication + state machine. + enum: + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - CancelledBeforePublish + - DeliveryConflict + - CredentialBlocked + - PreparationFailed + - PublicationOutcomeUnknown + type: string + terminalReason: + maxLength: 16384 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + verificationReceipt: + description: PublicationVerificationControlReceipt is an independent + remote observation. + properties: + descendantProofDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + expectedCommitSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + observedRemote: + description: |- + ControlRemoteRefState is an exact remote-ref observation. Absent and SHA are + mutually exclusive. The all-zero value is reserved for an explicitly unknown + observation in PublicationOutcomeUnknown receipts. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + operationId: + maxLength: 1024 + minLength: 1 + type: string + outcome: + description: PublicationControlState is the clean-room publication + state machine. + enum: + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - CancelledBeforePublish + - DeliveryConflict + - CredentialBlocked + - PreparationFailed + - PublicationOutcomeUnknown + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + verifiedAt: + format: date-time + type: string + required: + - expectedCommitSha + - observedRemote + - operationId + - outcome + - requestDigest + - verifiedAt + type: object + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: failure and unknown publication states require a terminal reason + rule: '!has(self.state) || !(self.state in [''DeliveryConflict'', ''CredentialBlocked'', + ''PreparationFailed'', ''PublicationOutcomeUnknown'']) || (has(self.terminalReason) + && size(self.terminalReason) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/repositorymonitor-customresourcedefinition.yaml b/charts/orka/crds/repositorymonitor-customresourcedefinition.yaml index ce98bfa57..9e2d52ca6 100644 --- a/charts/orka/crds/repositorymonitor-customresourcedefinition.yaml +++ b/charts/orka/crds/repositorymonitor-customresourcedefinition.yaml @@ -59,8 +59,8 @@ spec: description: RepositoryMonitorSpec defines the desired state of RepositoryMonitor. properties: agents: - description: Agents configures the agents used by monitor review and - repair tasks. + description: Agents configures the agents used by monitor review, + issue, and repair tasks. properties: implementer: description: Implementer is the agent used for guarded issue implementation @@ -76,6 +76,19 @@ spec: required: - name type: object + planner: + description: Planner is the agent used for issue planning tasks. + properties: + name: + description: Name is the name of the Agent + type: string + namespace: + description: Namespace is the namespace of the Agent (defaults + to Task namespace) + type: string + required: + - name + type: object repairer: description: Repairer is the agent used for repair tasks. properties: @@ -89,6 +102,19 @@ spec: required: - name type: object + researcher: + description: Researcher is the agent used for issue research tasks. + properties: + name: + description: Name is the name of the Agent + type: string + namespace: + description: Namespace is the namespace of the Agent (defaults + to Task namespace) + type: string + required: + - name + type: object reviewer: description: Reviewer is the agent used for pull-request review tasks. @@ -103,6 +129,19 @@ spec: required: - name type: object + triager: + description: Triager is the agent used for issue triage tasks. + properties: + name: + description: Name is the name of the Agent + type: string + namespace: + description: Namespace is the namespace of the Agent (defaults + to Task namespace) + type: string + required: + - name + type: object type: object automerge: description: Automerge controls deterministic merge behavior. @@ -152,6 +191,87 @@ spec: type: string type: object x-kubernetes-map-type: atomic + issueWorkflow: + description: IssueWorkflow controls issue triage, research, planning, + and implementation behavior. + properties: + implementation: + description: Implementation controls bounded implementation tasks. + properties: + allowedPaths: + description: |- + AllowedPaths optionally restricts implementation patch files to these path globs/prefixes. + Examples: api/**, internal/**, docs/**. + items: + type: string + type: array + x-kubernetes-list-type: set + branchPrefix: + description: BranchPrefix is the branch prefix for implementation + push branches. Defaults to orka/issue. + type: string + enabled: + description: Enabled enables implementation. Defaults to true + when an implementer agent is configured and a command requests + it. + type: boolean + maxActive: + description: MaxActive bounds concurrently active issue implementation/mutation + jobs per monitor. Defaults to 2. + format: int32 + minimum: 0 + type: integer + maxAttemptsPerIssue: + description: MaxAttemptsPerIssue bounds implementation attempts + for one issue. Defaults to 2. + format: int32 + minimum: 0 + type: integer + maxChangedFiles: + description: MaxChangedFiles bounds changed files in an implementation + patch. Defaults to 12. + format: int32 + minimum: 1 + type: integer + requireApprovedPlan: + description: RequireApprovedPlan blocks implementation unless + the latest plan was approved. + type: boolean + type: object + planning: + description: Planning controls read-only implementation plan generation. + properties: + enabled: + description: Enabled enables planning. Defaults to true when + a planner agent is configured and a command requests it. + type: boolean + requireHumanApprovalFor: + description: RequireHumanApprovalFor names risk levels or + plan categories that require explicit approval. + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + research: + description: Research controls read-only issue research. + properties: + enabled: + description: Enabled enables this phase. Defaults to true + when the corresponding agent is configured and a command + requests it. + type: boolean + type: object + triage: + description: Triage controls read-only issue classification. + properties: + enabled: + description: Enabled enables this phase. Defaults to true + when the corresponding agent is configured and a command + requests it. + type: boolean + type: object + type: object owner: description: Owner is the repository owner or organization. type: string @@ -389,6 +509,20 @@ spec: enabled: description: Enabled enables issue monitoring. type: boolean + excludeLabels: + description: ExcludeLabels excludes matching issues from actionable + inventory. + items: + type: string + type: array + x-kubernetes-list-type: set + includeLabels: + description: IncludeLabels optionally restricts issue inventory + to issues with any of these labels. + items: + type: string + type: array + x-kubernetes-list-type: set maxPerRun: description: MaxPerRun limits issues selected by one background run. @@ -421,6 +555,79 @@ spec: timeZone: description: TimeZone is the IANA time zone for the schedule. type: string + triggers: + description: Triggers configures external events that create durable + monitor commands. + properties: + github: + description: GitHub configures GitHub webhook triggers. + properties: + labels: + description: Labels maps GitHub labels to durable RepositoryMonitor + commands. + properties: + consumeCommandLabels: + description: ConsumeCommandLabels removes accepted one-shot + command labels after durable intake. + type: boolean + enabled: + description: Enabled enables durable command intake for + configured GitHub labels. + type: boolean + issues: + description: Issues maps issue command intents to label + names. Empty fields use the default orka:* labels. + properties: + approvePlan: + type: string + decompose: + type: string + implement: + type: string + plan: + type: string + research: + type: string + resume: + type: string + stop: + type: string + triage: + type: string + type: object + pullRequests: + description: PullRequests maps pull-request command intents + to label names. Empty fields use the default orka:* + labels. + properties: + automerge: + type: string + fix: + type: string + fixCI: + type: string + resume: + type: string + review: + type: string + stop: + type: string + updateBranch: + type: string + type: object + requireActorPermission: + default: write + description: |- + RequireActorPermission is the minimum GitHub permission for mutating/code-executing commands. + Supported values are write, maintain, and admin. Defaults to write. + enum: + - write + - maintain + - admin + type: string + type: object + type: object + type: object validation: description: Validation configures deterministic validation commands for repair. @@ -450,6 +657,11 @@ spec: description: ActiveRepairs is the count of repair jobs currently active. format: int32 type: integer + blockedIssues: + description: BlockedIssues is the count of issues blocked by guard + labels or workflow policy. + format: int32 + type: integer blockedItems: description: BlockedItems is the count of items blocked by policy, failures, or human action. @@ -539,11 +751,21 @@ spec: in status. format: int64 type: integer + openIssues: + description: OpenIssues is the current count of open issues seen by + the monitor. + format: int32 + type: integer openPullRequests: description: OpenPullRequests is the current count of open pull requests seen by the monitor. format: int32 type: integer + pendingIssueActions: + description: PendingIssueActions is the count of issues waiting for + a queued workflow action. + format: int32 + type: integer pendingReviews: description: PendingReviews is the count of items waiting for review. format: int32 diff --git a/charts/orka/crds/runtimepool-customresourcedefinition.yaml b/charts/orka/crds/runtimepool-customresourcedefinition.yaml new file mode 100644 index 000000000..3100c3d7d --- /dev/null +++ b/charts/orka/crds/runtimepool-customresourcedefinition.yaml @@ -0,0 +1,638 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: runtimepools.core.orka.ai +spec: + group: core.orka.ai + names: + kind: RuntimePool + listKind: RuntimePoolList + plural: runtimepools + shortNames: + - rtpool + singular: runtimepool + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.lifecycle + name: Lifecycle + type: string + - jsonPath: .status.admissionState + name: Admission + type: string + - jsonPath: .status.desiredReplicas + name: Desired + type: integer + - jsonPath: .status.currentReplicas + name: Current + type: integer + - jsonPath: .status.capacity.residentSessions + name: Sessions + type: integer + - jsonPath: .status.capacity.runningPrompts + name: Prompts + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: RuntimePool is the Schema for controller-owned ACP runtime pools. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + RuntimePoolSpec defines the desired state of a controller-owned logical pool. + Trust-domain placement and the runtime image/profile are immutable; rollout + uses drain-and-replace rather than changing an in-memory instance in place. + properties: + capacity: + default: + maxResidentSessions: 10 + maxRunningPrompts: 4 + description: Capacity sets resident-session and running-prompt limits. + properties: + maxResidentSessions: + default: 10 + description: MaxResidentSessions is the maximum number of resident + RuntimeSessions. + format: int32 + maximum: 1000 + minimum: 1 + type: integer + maxRunningPrompts: + default: 4 + description: MaxRunningPrompts is the maximum number of concurrently + running prompts. + format: int32 + maximum: 1000 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: maxRunningPrompts cannot exceed maxResidentSessions + rule: self.maxRunningPrompts <= self.maxResidentSessions + coldStartTimeoutSeconds: + default: 120 + description: ColdStartTimeoutSeconds bounds a 0 -> 1 startup before + the pool is marked degraded. + format: int32 + maximum: 3600 + minimum: 1 + type: integer + desiredReplicas: + default: 0 + description: |- + DesiredReplicas is zero or one. More than one runtime Pod would make + stateful exact-instance routing ambiguous. + format: int32 + maximum: 1 + minimum: 0 + type: integer + runtime: + description: Runtime pins the immutable supervisor image and behavior + profile. + properties: + image: + description: Image is a digest-pinned OCI image. Mutable tags + are intentionally rejected. + maxLength: 2048 + pattern: ^[^\s@]+@sha256:[a-f0-9]{64}$ + type: string + profile: + description: Profile is the immutable runtime profile enforced + for every active instance. + properties: + acpProfile: + description: ACPProfile is the reviewed ACP wire/profile identifier. + enum: + - acp.v1 + type: string + adapterDigests: + additionalProperties: + type: string + description: AdapterDigests pins every adapter and provider + CLI artifact used by the pool. + maxProperties: 32 + minProperties: 1 + type: object + agentConfigurationDigest: + description: AgentConfigurationDigest freezes non-secret Agent/runtime + configuration. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + approvalPolicyDigest: + description: ApprovalPolicyDigest freezes the effective approval + policy. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digest: + description: Digest is the canonical immutable runtime-profile + digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digestSchemaVersion: + description: DigestSchemaVersion identifies the canonicalization + schema used to compute Digest. + maxLength: 64 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ + type: string + mcpConfigurationDigest: + description: MCPConfigurationDigest freezes prompt-scoped + broker/MCP configuration. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + model: + description: Model is the exact reviewed model identifier. + maxLength: 256 + minLength: 1 + type: string + protocolVersion: + default: orka.harness.v2 + description: ProtocolVersion is the controller-to-supervisor + protocol profile. + enum: + - orka.harness.v2 + type: string + providerKind: + description: ProviderKind selects the one provider adapter + present in the immutable image. + enum: + - codex + - claude + - copilot + type: string + proxyCredentialRole: + description: ProxyCredentialRole identifies the provider-proxy + client role, never a secret value. + maxLength: 256 + minLength: 1 + type: string + proxyCredentialScope: + description: ProxyCredentialScope is the bounded model/session + capability scope. + maxLength: 1024 + minLength: 1 + type: string + resourceClass: + description: ResourceClass is the controller-supported pool + resource class included in Digest. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$ + type: string + toolPolicyDigest: + description: ToolPolicyDigest freezes the effective tool allow/deny + policy. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + workspaceIntent: + description: WorkspaceIntent is part of the immutable runtime + profile. + enum: + - read + - write + type: string + required: + - acpProfile + - adapterDigests + - agentConfigurationDigest + - approvalPolicyDigest + - digest + - digestSchemaVersion + - mcpConfigurationDigest + - model + - providerKind + - proxyCredentialRole + - proxyCredentialScope + - resourceClass + - toolPolicyDigest + - workspaceIntent + type: object + required: + - image + - profile + type: object + runtimeNamespace: + description: |- + RuntimeNamespace is the physical namespace for controller-owned runtime + resources. When omitted, the controller selects its configured runtime namespace. + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + trustDomain: + description: TrustDomain is the logical namespace/identity boundary + served by this pool. + properties: + identity: + description: |- + Identity is the controller-defined, canonical trust-domain identity. It + must remain stable across physical runtime namespace or Pod replacement. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: Namespace is the Task namespace represented by this + trust domain. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - identity + - namespace + type: object + required: + - runtime + - trustDomain + type: object + x-kubernetes-validations: + - message: trustDomain is immutable + rule: self.trustDomain == oldSelf.trustDomain + - message: runtimeNamespace is immutable + rule: has(self.runtimeNamespace) == has(oldSelf.runtimeNamespace) && + (!has(self.runtimeNamespace) || self.runtimeNamespace == oldSelf.runtimeNamespace) + - message: runtime image and profile are immutable + rule: self.runtime == oldSelf.runtime + status: + description: RuntimePoolStatus defines the observed state of a controller-owned + pool. + properties: + activeInstance: + description: |- + ActiveInstance is the exact selected Pod and supervisor boot. It is empty + unless one instance has been authoritatively selected. + properties: + bootID: + description: BootID is the immutable supervisor boot identifier + inside the selected Pod. + maxLength: 128 + minLength: 1 + type: string + controllerEpoch: + description: ControllerEpoch is the durable controller epoch to + which this instance is bound. + format: int64 + minimum: 1 + type: integer + lastObservedTime: + description: LastObservedTime is the last authenticated status + observation for this instance. + format: date-time + type: string + podAddress: + description: |- + PodAddress is the exact Pod address used for stateful routing, not a + load-balanced Service endpoint. + maxLength: 253 + minLength: 1 + type: string + podName: + description: PodName is the exact selected runtime Pod name. + maxLength: 253 + minLength: 1 + type: string + podNamespace: + description: PodNamespace is the namespace containing the selected + runtime Pod. + maxLength: 63 + minLength: 1 + type: string + podUID: + description: PodUID is the Kubernetes UID of the selected Pod. + maxLength: 128 + minLength: 1 + type: string + profileDigest: + description: ProfileDigest is the immutable runtime-profile digest + advertised by this instance. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + profileDigestSchemaVersion: + description: ProfileDigestSchemaVersion is the digest schema advertised + by this instance. + maxLength: 64 + minLength: 1 + type: string + protocolVersion: + description: ProtocolVersion is the supervisor protocol actually + advertised by this instance. + enum: + - orka.harness.v2 + type: string + providerTokenGeneration: + description: |- + ProviderTokenGeneration is a non-secret digest generation for the exact + provider capability mounted into this runtime Pod. It lets the controller + prove that a selected instance converged on the intended proxy credential + without exposing the bearer token. + pattern: ^[a-f0-9]{16}$ + type: string + runtimeInstanceID: + description: RuntimeInstanceID is the portable v2 instance fence + derived from PodUID and BootID. + maxLength: 253 + minLength: 1 + type: string + required: + - bootID + - controllerEpoch + - podAddress + - podName + - podNamespace + - podUID + - profileDigest + - profileDigestSchemaVersion + - protocolVersion + - providerTokenGeneration + - runtimeInstanceID + type: object + admissionState: + description: AdmissionState is the authoritative admission gate for + new RuntimeSessions. + enum: + - Closed + - Accepting + - Draining + - Ambiguous + type: string + capacity: + description: Capacity reports effective limits, use, and queued demand. + properties: + finalizingSessions: + description: FinalizingSessions is the count reserved for validation, + publication, or finalization. + format: int32 + minimum: 0 + type: integer + liveDescendants: + description: LiveDescendants is the authenticated count of tracked + runtime descendants. + format: int32 + minimum: 0 + type: integer + maxResidentSessions: + description: MaxResidentSessions is the effective configured resident-session + limit. + format: int32 + minimum: 0 + type: integer + maxRunningPrompts: + description: MaxRunningPrompts is the effective configured running-prompt + limit. + format: int32 + minimum: 0 + type: integer + pendingPermissions: + description: PendingPermissions is the authenticated count of + unresolved prompt permissions. + format: int32 + minimum: 0 + type: integer + queuedTasks: + description: QueuedTasks is durable unsatisfied demand assigned + to this pool. + format: int32 + minimum: 0 + type: integer + reservations: + description: |- + Reservations is the bounded authoritative set of coordinator-owned + pre-admission capacity claims. + items: + description: |- + RuntimePoolCapacityReservationStatus is one durable, exact-instance capacity + claim. The composite key is the pool UID, Task UID, attempt, and controller + epoch. A reservation claims resident-session and prompt admission slots until + the supervisor accepts the corresponding work or the reservation expires. + properties: + attempt: + description: Attempt is the Task attempt that owns the claim. + format: int32 + minimum: 1 + type: integer + controllerEpoch: + description: ControllerEpoch fences the claim to one controller + leadership epoch. + format: int64 + minimum: 1 + type: integer + expiresAt: + description: |- + ExpiresAt is renewed while pre-admission work is active. A later + dispatcher may reclaim the claim after this time. + format: date-time + type: string + poolUID: + description: PoolUID fences the claim to the exact RuntimePool + object. + maxLength: 128 + minLength: 1 + type: string + promptSlots: + description: PromptSlots is one until the prompt is accepted + by the supervisor. + format: int32 + maximum: 1 + minimum: 0 + type: integer + reservedAt: + description: ReservedAt is the first successful resource-version + CAS for this claim. + format: date-time + type: string + residentSlots: + description: |- + ResidentSlots is zero after a RuntimeSession is admitted and one while a + new resident-session slot is still reserved. + format: int32 + maximum: 1 + minimum: 0 + type: integer + runtimeInstanceID: + description: RuntimeInstanceID binds admission to the exact + selected Pod/boot pair. + maxLength: 253 + minLength: 1 + type: string + taskUID: + description: TaskUID is the immutable Task identity that + owns the claim. + maxLength: 128 + minLength: 1 + type: string + required: + - attempt + - controllerEpoch + - expiresAt + - poolUID + - promptSlots + - reservedAt + - residentSlots + - runtimeInstanceID + - taskUID + type: object + x-kubernetes-validations: + - message: a capacity reservation must claim at least one slot + rule: self.residentSlots + self.promptSlots > 0 + maxItems: 1000 + type: array + x-kubernetes-list-map-keys: + - poolUID + - taskUID + - attempt + - controllerEpoch + x-kubernetes-list-type: map + reservedPrompts: + description: ReservedPrompts is the sum of prompt slots in Reservations. + format: int32 + minimum: 0 + type: integer + reservedSessions: + description: ReservedSessions is the sum of resident slots in + Reservations. + format: int32 + minimum: 0 + type: integer + residentSessions: + description: ResidentSessions is the authenticated supervisor + count of resident sessions. + format: int32 + minimum: 0 + type: integer + runningPrompts: + description: RunningPrompts is the authenticated supervisor count + of active prompts. + format: int32 + minimum: 0 + type: integer + type: object + conditions: + description: |- + Conditions report admission, Pod Security, quota, scheduling, rollout, and + other controller-observed failures. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerEpoch: + description: ControllerEpoch is the durable epoch required for authoritative + pool writes. + format: int64 + minimum: 0 + type: integer + currentReplicas: + description: CurrentReplicas is the number of non-terminated runtime + Pods owned by the pool. + format: int32 + minimum: 0 + type: integer + desiredReplicas: + description: DesiredReplicas is the desired replica count observed + by the controller. + format: int32 + maximum: 1 + minimum: 0 + type: integer + lifecycle: + description: Lifecycle is the explicit pool lifecycle. + enum: + - Stopped + - Starting + - Serving + - Draining + - Quiescent + - Stopping + - Degraded + - Ambiguous + type: string + message: + description: Message contains bounded, sanitized reconciliation context. + maxLength: 1024 + type: string + observedGeneration: + description: ObservedGeneration is the latest RuntimePool generation + reconciled by the controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/runtimesessioncontrol-customresourcedefinition.yaml b/charts/orka/crds/runtimesessioncontrol-customresourcedefinition.yaml new file mode 100644 index 000000000..010bfd9d7 --- /dev/null +++ b/charts/orka/crds/runtimesessioncontrol-customresourcedefinition.yaml @@ -0,0 +1,309 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: runtimesessioncontrols.core.orka.ai +spec: + group: core.orka.ai + names: + kind: RuntimeSessionControl + listKind: RuntimeSessionControlList + plural: runtimesessioncontrols + shortNames: + - rsctrl + singular: runtimesessioncontrol + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.lifecycle + name: Lifecycle + type: string + - jsonPath: .status.availability + name: Availability + type: string + - jsonPath: .status.generation + name: Generation + type: integer + - jsonPath: .status.mutationLeaseGeneration + name: Lease + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + RuntimeSessionControl is the Kubernetes-authoritative RuntimeSession control + record. SessionTurn/transcript/deferred-outbox data remains in one durable + SQLite transaction; the Kubernetes store completes the authoritative + SessionControl/BranchClaim CAS before activating the terminal projection. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + RuntimeSessionControlSpec contains immutable session identity, ownership, and + profile bindings. Profile changes create a new session generation in status; + they do not mutate this immutable record identity. + properties: + owner: + description: Owner identifies the immutable Task or durable Session + owner. + properties: + kind: + enum: + - Task + - Session + - RuntimePool + - PromptAttempt + type: string + uid: + maxLength: 1024 + minLength: 1 + type: string + required: + - kind + - uid + type: object + profileDigestSchemaVersion: + description: ProfileDigestSchemaVersion identifies how RuntimeProfileDigest + was built. + maxLength: 64 + type: string + requestDigest: + description: RequestDigest binds creation to exact canonical input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimePoolRef: + description: RuntimePoolRef is the controller-owned logical pool name + when known. + maxLength: 253 + type: string + runtimePoolUid: + description: RuntimePoolUID fences the pool object across delete/recreate. + maxLength: 1024 + type: string + runtimeProfileDigest: + description: RuntimeProfileDigest binds the session to immutable runtime + behavior. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + sessionName: + description: |- + SessionName is the immutable user-visible Session key within the object + namespace. The Kubernetes object name is a digest-derived storage key and + must not be treated as the Session name. + maxLength: 1024 + minLength: 1 + type: string + sessionUid: + description: SessionUID is the immutable Orka Session identity. + maxLength: 1024 + minLength: 1 + type: string + required: + - owner + - requestDigest + - sessionName + - sessionUid + type: object + x-kubernetes-validations: + - message: runtime session control spec is immutable + rule: self == oldSelf + status: + description: |- + RuntimeSessionControlStatus contains the lifecycle, generation, mutation + Lease, and independently verified recovery baseline. + properties: + availability: + description: RuntimeSessionControlAvailability gates the Session mutation + lease. + enum: + - Available + - ReconciliationBlocked + type: string + blockedReason: + maxLength: 16384 + type: string + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + generation: + description: Generation is the monotonic ACP RuntimeSession generation. + format: int64 + minimum: 1 + type: integer + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + lifecycle: + description: RuntimeSessionControlLifecycle is the durable RuntimeSession + lifecycle. + enum: + - Creating + - Idle + - PromptRunning + - Validating + - PreparingPublication + - PublicationPrepared + - Publishing + - Verifying + - Finalizing + - Cancelling + - Poisoned + - Deleting + - Deleted + type: string + mutationLease: + description: |- + RuntimeSessionMutationLeaseStatus mirrors the namespaced Kubernetes Lease + that serializes mutation for one immutable SessionUID. + properties: + acquiredAt: + format: date-time + type: string + attempt: + format: int64 + minimum: 1 + type: integer + expiresAt: + format: date-time + type: string + generation: + format: int64 + minimum: 1 + type: integer + leaseName: + maxLength: 253 + minLength: 1 + type: string + leaseResourceVersion: + maxLength: 64 + minLength: 1 + type: string + promptId: + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + taskUid: + maxLength: 1024 + minLength: 1 + type: string + required: + - acquiredAt + - attempt + - generation + - leaseName + - leaseResourceVersion + - promptId + - requestDigest + - taskUid + type: object + mutationLeaseGeneration: + description: MutationLeaseGeneration is monotonic and never reused + for SessionUID. + format: int64 + minimum: 0 + type: integer + relatedPromptAttemptId: + maxLength: 1024 + type: string + relatedPublicationId: + maxLength: 1024 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + verifiedBaseline: + description: ControlVerifiedBranchBaseline is an independently verified + branch baseline. + properties: + ref: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + repositoryId: + maxLength: 1024 + minLength: 1 + type: string + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - ref + - repositoryId + - sha + type: object + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: available sessions must clear reconciliation block metadata + rule: '!has(self.availability) || self.availability != ''Available'' + || ((!has(self.blockedReason) || size(self.blockedReason) == 0) && + (!has(self.relatedPromptAttemptId) || size(self.relatedPromptAttemptId) + == 0) && (!has(self.relatedPublicationId) || size(self.relatedPublicationId) + == 0))' + - message: reconciliation-blocked sessions require a reason + rule: '!has(self.availability) || self.availability != ''ReconciliationBlocked'' + || (has(self.blockedReason) && size(self.blockedReason) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/orka/crds/task-customresourcedefinition.yaml b/charts/orka/crds/task-customresourcedefinition.yaml index 0c0354bf3..296cb5282 100644 --- a/charts/orka/crds/task-customresourcedefinition.yaml +++ b/charts/orka/crds/task-customresourcedefinition.yaml @@ -92,52 +92,6 @@ spec: maximum: 1000 minimum: 1 type: integer - workspace: - description: Workspace defines the working directory configuration - properties: - branch: - description: Branch is the git branch to checkout - type: string - forkRepo: - description: ForkRepo is the writable fork repository URL - for pushing changes - type: string - gitRepo: - description: GitRepo is the repository URL to clone - type: string - gitSecretRef: - description: GitSecretRef references a Secret containing git - credentials - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - prBaseBranch: - description: PRBaseBranch is the upstream branch to target - for pull requests - type: string - pushBranch: - description: |- - PushBranch is the remote branch name to push changes to after the agent completes. - When set, FinalizeResult will commit and push changes to this branch. - type: string - ref: - description: Ref is a specific git ref (commit SHA, tag) to - checkout - type: string - subPath: - description: SubPath is a subdirectory within the repo to - use as workspace root - type: string - type: object type: object ai: description: AI contains AI-specific configuration (when type is "ai") @@ -1367,10 +1321,10 @@ spec: type: array workspace: description: |- - Workspace requests an upstream agent-sandbox execution workspace for agent Tasks. - When enabled, the Task controller validates the request and propagates the - resolved sandbox settings to the agent worker Job. The worker wrapper then - claims the sandbox workspace and runs the configured agent runtime inside it. + Workspace requests an execution workspace for worker-backed Task types. + ACP core agent Tasks reject this field because their ephemeral workspace is + owned by RuntimeSession lifecycle and clean-room publication. Actor-backed + RuntimeSession support is a future integration behind the v2 lifecycle seam. properties: boot: description: |- @@ -1378,6 +1332,18 @@ spec: instead of resuming from the provider's default snapshot. Currently supported by the Substrate provider. type: boolean + classRef: + description: |- + ClassRef selects an immutable ExecutionWorkspaceClass in the Task namespace. Setting + classRef implicitly enables the controller-first workspace path. + properties: + name: + description: Name is the class name. + minLength: 1 + type: string + required: + - name + type: object cleanupPolicy: description: |- CleanupPolicy controls whether the workspace is deleted or retained after use. @@ -1411,6 +1377,13 @@ spec: worker derives a stable key from namespace, template, and reuse key. type: string type: object + onDetach: + description: OnDetach requests an action allowed by the selected + class. + enum: + - Suspend + - Delete + type: string poolRef: description: |- PoolRef references an operator-managed Substrate actor pool for placement, @@ -1479,7 +1452,22 @@ spec: It defaults to the Task namespace, or the controller namespace when configured. type: string type: object + workspaceSlot: + default: default + description: WorkspaceSlot names one independently reusable + workspace within a Session. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string type: object + x-kubernetes-validations: + - message: classRef cannot be combined with legacy enabled, provider, + template, pool, cleanup, boot, snapshot, or hibernation settings + rule: '!has(self.classRef) || (!has(self.provider) && !has(self.templateRef) + && !has(self.poolRef) && (!has(self.enabled) || !self.enabled) + && !has(self.cleanupPolicy) && (!has(self.boot) || !self.boot) + && !has(self.snapshot) && !has(self.hibernation))' type: object failedRunsHistoryLimit: default: 1 @@ -1769,51 +1757,244 @@ spec: type: string workspace: description: |- - Workspace defines repository checkout and push settings for tasks that need - a git workspace. Agent tasks can continue to use agentRuntime.workspace for - compatibility; this top-level field is used by container tasks as well. + Workspace defines the canonical repository workspace, intent, credentials, + and publication request. Agent Tasks that omit intent are interpreted as + read by controller logic; an omitted intent preserves existing container behavior. properties: + allowedPaths: + description: |- + AllowedPaths restricts publishable workspace changes to these path globs or + directory prefixes ending in /**. Empty allows every otherwise-safe path. + items: + type: string + maxItems: 256 + type: array branch: - description: Branch is the git branch to checkout + description: Branch is the source branch to check out. + maxLength: 255 type: string - forkRepo: - description: ForkRepo is the writable fork repository URL for - pushing changes + createPR: + default: false + description: |- + CreatePR explicitly requests pull request reconciliation after branch publication. + Branch push remains the minimum durable delivery when false. + type: boolean + denyRepositoryControlPaths: + description: |- + DenyRepositoryControlPaths rejects workflow, RBAC, and chart-secret paths + before publication even when AllowedPaths is empty or otherwise matches. + type: boolean + expectedRemoteSHA: + description: |- + ExpectedRemoteSHA requires the publication branch to exist at this exact + commit before publication. Empty means the branch must be absent. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ type: string + forgeCredentialRef: + description: |- + ForgeCredentialRef references the forge API credential used only for pull + request reconciliation when createPR=true. + properties: + key: + default: token + description: |- + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object gitRepo: - description: GitRepo is the repository URL to clone + description: |- + GitRepo is the source repository URL cloned by the clean-room workspace boundary. + Credentials must not be embedded in the URL. + maxLength: 2048 + type: string + intent: + description: |- + Intent declares whether the verified workspace must remain unchanged or may + produce a publication artifact. It is immutable for the lifetime of the Task. + Agent Tasks that omit intent are interpreted as read by controller logic; + omitted intent preserves the existing behavior of container Tasks. + enum: + - read + - write + type: string + maxChangedFiles: + description: |- + MaxChangedFiles bounds the total changed, deleted, and symlink paths accepted + from the trusted supervisor before publication. Zero uses the runtime limit. + format: int32 + minimum: 1 + type: integer + prBaseBranch: + description: PRBaseBranch is the upstream branch targeted when + CreatePR is true. + maxLength: 255 type: string - gitSecretRef: - description: GitSecretRef references a Secret containing git credentials + publicationCredentialRef: + description: |- + PublicationCredentialRef references the target-repository write credential + used only for the exact CAS push. It is never used to clone the source. properties: - name: - default: "" + key: + default: token description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + required: + - name type: object - x-kubernetes-map-type: atomic - prBaseBranch: - description: PRBaseBranch is the upstream branch to target for - pull requests + publicationGitRepo: + description: |- + PublicationGitRepo is the repository URL whose branch receives an exact CAS publication. + Credentials must not be embedded in the URL. + maxLength: 2048 type: string + publicationReadCredentialRef: + description: |- + PublicationReadCredentialRef references the target-repository read + credential used only for preflight and independent post-push verification. + properties: + key: + default: token + description: |- + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + publicationRepository: + description: PublicationRepository is the canonical provider identity + for PublicationGitRepo. + properties: + id: + description: ID is the provider's canonical, stable repository + identifier. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object pushBranch: description: |- - PushBranch is the remote branch name to push changes to after the agent completes. - When set, FinalizeResult will commit and push changes to this branch. + PushBranch is the publication branch. For write Tasks the controller derives + a full-entropy Task- or Session-owned branch when this is omitted. + maxLength: 255 type: string + readCredentialRef: + description: |- + ReadCredentialRef references the one-operation clone/read credential Secret. + The Secret is resolved only by the clean-room workspace boundary. + properties: + key: + default: token + description: |- + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object ref: - description: Ref is a specific git ref (commit SHA, tag) to checkout + description: Ref is a specific source git ref, commit SHA, or + tag to check out. + maxLength: 512 type: string + rejectBinaryFiles: + description: RejectBinaryFiles rejects changed file content that + is not valid text. + type: boolean + rejectSecretLikeContent: + description: |- + RejectSecretLikeContent applies Orka's generic secret detector to changed + paths and file contents before publication. + type: boolean + sourceRepository: + description: SourceRepository is the canonical provider identity + for GitRepo when available. + properties: + id: + description: ID is the provider's canonical, stable repository + identifier. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object subPath: - description: SubPath is a subdirectory within the repo to use - as workspace root + description: SubPath is a subdirectory within the source repository + used as workspace root. + maxLength: 1024 type: string type: object + x-kubernetes-validations: + - message: createPR requires write workspace intent + rule: '!self.createPR || self.intent == ''write''' + - message: gitRepo must not contain embedded credentials, query parameters, + or fragments + rule: '!has(self.gitRepo) || (!self.gitRepo.matches(''(?i)^[A-Za-z][A-Za-z0-9+.-]*://[^/]*@'') + && !self.gitRepo.contains(''?'') && !self.gitRepo.contains(''#''))' + - message: publicationGitRepo must not contain embedded credentials, + query parameters, or fragments + rule: '!has(self.publicationGitRepo) || (!self.publicationGitRepo.matches(''(?i)^[A-Za-z][A-Za-z0-9+.-]*://[^/]*@'') + && !self.publicationGitRepo.contains(''?'') && !self.publicationGitRepo.contains(''#''))' required: - type type: object @@ -1824,6 +2005,31 @@ spec: - message: transaction is immutable rule: has(self.transaction) == has(oldSelf.transaction) && (!has(self.transaction) || self.transaction == oldSelf.transaction) + - message: type is immutable + rule: self.type == oldSelf.type + - message: effective workspace intent is immutable + rule: '(has(self.workspace) && has(self.workspace.intent) ? self.workspace.intent + : (self.type == ''agent'' ? ''read'' : self.type)) == (has(oldSelf.workspace) + && has(oldSelf.workspace.intent) ? oldSelf.workspace.intent : (oldSelf.type + == ''agent'' ? ''read'' : oldSelf.type))' + - message: agent prompt is immutable + rule: self.type != 'agent' || (has(self.prompt) == has(oldSelf.prompt) + && (!has(self.prompt) || self.prompt == oldSelf.prompt)) + - message: agentRef is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.agentRef) == has(oldSelf.agentRef) + && (!has(self.agentRef) || self.agentRef == oldSelf.agentRef)) + - message: agentRuntime is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.agentRuntime) == has(oldSelf.agentRuntime) + && (!has(self.agentRuntime) || self.agentRuntime == oldSelf.agentRuntime)) + - message: workspace is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.workspace) == has(oldSelf.workspace) + && (!has(self.workspace) || self.workspace == oldSelf.workspace)) + - message: timeout is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.timeout) == has(oldSelf.timeout) + && (!has(self.timeout) || self.timeout == oldSelf.timeout)) + - message: session workspace reuse requires spec.sessionRef + rule: '!has(self.execution) || !has(self.execution.workspace) || self.execution.workspace.reusePolicy + != ''session'' || has(self.sessionRef)' status: description: TaskStatus defines the observed state of Task properties: @@ -1849,6 +2055,7 @@ spec: enum: - Pending - Running + - Finalizing - Succeeded - Failed - Scheduled @@ -1927,18 +2134,484 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + delivery: + description: Delivery reports trusted workspace validation and publication + reconciliation. + properties: + artifactDigest: + description: ArtifactDigest is the durable content-addressed workspace + delta digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + branch: + description: Branch is the publication branch without a refs/heads/ + prefix. + maxLength: 255 + type: string + expectedCommitSHA: + description: ExpectedCommitSHA is the exact Orka-owned commit + prepared for publication. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + lastTransitionTime: + description: LastTransitionTime is the last durable delivery-state + transition time. + format: date-time + type: string + message: + description: Message contains bounded, sanitized delivery context. + maxLength: 1024 + type: string + outcome: + description: Outcome is set only after delivery reaches a terminal + classification. + enum: + - NotRequested + - VerifiedExact + - DeliveredSuperseded + - ReadValidated + - NoChange + - CancelledBeforePublish + - ReadOnlyWorkspaceModified + - DeliveryConflict + - CredentialBlocked + - PublicationOutcomeUnknown + type: string + prReceipt: + description: PRReceipt is present only when createPR was explicitly + requested and reconciled. + properties: + baseBranch: + description: BaseBranch is the reconciled pull request base + branch. + maxLength: 255 + type: string + headBranch: + description: HeadBranch is the reconciled pull request head + branch. + maxLength: 255 + type: string + headSHA: + description: HeadSHA is the exact observed pull request head + commit. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + id: + description: ID is the provider's durable pull request identifier. + maxLength: 512 + minLength: 1 + type: string + number: + description: Number is the provider's numeric pull request + number when available. + format: int64 + minimum: 1 + type: integer + state: + description: State is the provider-observed pull request state. + maxLength: 64 + type: string + url: + description: URL is the canonical user-facing pull request + URL. It must not contain credentials. + maxLength: 2048 + type: string + required: + - id + type: object + publicationID: + description: PublicationID is the durable identity reused for + reconciliation of the same artifact. + maxLength: 253 + pattern: ^[A-Za-z0-9][A-Za-z0-9._:-]{0,252}$ + type: string + publicationRepository: + description: PublicationRepository is the canonical repository + whose branch was reconciled. + properties: + id: + description: ID is the provider's canonical, stable repository + identifier. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object + reason: + description: Reason is a stable machine-readable explanation for + State or Outcome. + maxLength: 128 + pattern: ^[A-Za-z][A-Za-z0-9._-]{0,127}$ + type: string + remoteBeforeSHA: + description: |- + RemoteBeforeSHA is the exact publication ref observed before the CAS push. + Nil means not yet observed; a pointer to the empty string records explicit + absence; a non-empty value records the observed object ID. + pattern: ^(|[a-f0-9]{40}|[a-f0-9]{64})$ + type: string + sourceRepository: + description: SourceRepository is the canonical repository from + which the workspace baseline was created. + properties: + id: + description: ID is the provider's canonical, stable repository + identifier. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object + startingSHA: + description: StartingSHA is the verified source baseline before + prompt execution. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + state: + description: State is the current durable delivery state. + enum: + - NotRequested + - Validating + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - ReadValidated + - NoChange + - CancelledBeforePublish + - ReadOnlyWorkspaceModified + - DeliveryConflict + - CredentialBlocked + - PublicationOutcomeUnknown + type: string + supersedingRemoteSHA: + description: SupersedingRemoteSHA is the verified descendant that + superseded ExpectedCommitSHA. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + treeSHA: + description: TreeSHA is the deterministic clean-room tree written + by the publisher. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + verifiedRemoteSHA: + description: VerifiedRemoteSHA is the independently observed remote + branch head. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + type: object + x-kubernetes-validations: + - message: delivery outcome requires the matching terminal state + rule: '!has(self.outcome) || (has(self.state) && self.state == self.outcome)' + - message: terminal delivery state requires an outcome + rule: '!has(self.state) || !(self.state in [''NotRequested'', ''VerifiedExact'', + ''DeliveredSuperseded'', ''ReadValidated'', ''NoChange'', ''CancelledBeforePublish'', + ''ReadOnlyWorkspaceModified'', ''DeliveryConflict'', ''CredentialBlocked'', + ''PublicationOutcomeUnknown'']) || has(self.outcome)' + execution: + description: |- + Execution reports the durable execution state and terminal outcome for the + current attempt. Phase remains the compatibility projection. + properties: + agentRuntimeName: + description: |- + AgentRuntimeName is the namespaced external orka.harness.v2 registration + selected for this attempt. It is mutually exclusive with RuntimePoolName. + maxLength: 253 + type: string + agentRuntimeUID: + description: |- + AgentRuntimeUID is the immutable external AgentRuntime UID fenced into the + attempt selection. + maxLength: 253 + type: string + attempt: + description: Attempt is the one-based Task execution attempt represented + by this status. + format: int32 + minimum: 1 + type: integer + controllerEpoch: + description: ControllerEpoch is the durable controller epoch fencing + this attempt. + format: int64 + minimum: 0 + type: integer + forgeCredentialResourceVersion: + description: |- + ForgeCredentialResourceVersion freezes the forge-only Secret version used + for pull request reconciliation. + maxLength: 253 + type: string + lastTransitionTime: + description: LastTransitionTime is the last durable execution-state + transition time. + format: date-time + type: string + message: + description: Message contains bounded, sanitized execution context. + maxLength: 1024 + type: string + outcome: + description: Outcome is set only after execution reaches a terminal + classification. + enum: + - Succeeded + - Failed + - Cancelled + - OutcomeUnknown + type: string + promptID: + description: PromptID is the durable prompt identity used for + submission and settlement. + maxLength: 253 + type: string + publicationCredentialResourceVersion: + description: |- + PublicationCredentialResourceVersion freezes the target-write Secret + version selected at reservation without exposing credential material. + maxLength: 253 + type: string + publicationReadCredentialResourceVersion: + description: |- + PublicationReadCredentialResourceVersion freezes the target-read Secret + version used for preflight and independent verification. + maxLength: 253 + type: string + readCredentialResourceVersion: + description: |- + ReadCredentialResourceVersion freezes the read credential Secret version + selected at reservation without exposing credential material. + maxLength: 253 + type: string + reason: + description: Reason is a stable machine-readable explanation for + State or Outcome. + maxLength: 128 + pattern: ^[A-Za-z][A-Za-z0-9._-]{0,127}$ + type: string + requestDigest: + description: RequestDigest is the canonical immutable prompt request + digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimeInstanceID: + description: RuntimeInstanceID is the exact selected supervisor + Pod UID plus boot identity. + maxLength: 512 + type: string + runtimePoolName: + description: RuntimePoolName is the namespaced logical pool selected + for this attempt. + maxLength: 253 + type: string + runtimePoolUID: + description: RuntimePoolUID is the immutable pool UID fenced into + runtime requests. + maxLength: 253 + type: string + runtimeSessionCleanupDigest: + description: |- + RuntimeSessionCleanupDigest is the controller-owned proof that the exact + RuntimeSession requiring retirement was deleted or its immutable runtime + instance was replaced. Users may read but cannot mutate the Task status subresource. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimeSessionGeneration: + description: RuntimeSessionGeneration is the monotonic profile/session + generation. + format: int64 + minimum: 0 + type: integer + runtimeSessionUID: + description: RuntimeSessionUID is the stable controller-owned + Session execution identity. + maxLength: 253 + type: string + state: + description: State is the current durable execution state. + enum: + - Queued + - Reserved + - SessionStarting + - Planned + - Submitting + - SubmittedUnknown + - Accepted + - Running + - Settling + - Succeeded + - Failed + - Cancelled + - OutcomeUnknown + type: string + type: object + x-kubernetes-validations: + - message: execution outcome requires the matching terminal state + rule: '!has(self.outcome) || (has(self.state) && self.state == self.outcome)' + - message: terminal execution state requires an outcome + rule: '!has(self.state) || !(self.state in [''Succeeded'', ''Failed'', + ''Cancelled'', ''OutcomeUnknown'']) || has(self.outcome)' + executionOutcome: + description: |- + ExecutionOutcome records the immutable outcome of a non-ACP workload before + provider-neutral execution-workspace finalization completes. + properties: + attempt: + description: Attempt is the Task attempt that produced this outcome. + format: int32 + minimum: 1 + type: integer + message: + description: Message contains sanitized execution context. + type: string + phase: + allOf: + - enum: + - Pending + - Running + - Finalizing + - Succeeded + - Failed + - Scheduled + - Cancelled + - enum: + - Succeeded + - Failed + - Cancelled + description: Phase is the terminal workload execution phase. + type: string + recordedAt: + description: RecordedAt is when Orka durably recorded the execution + outcome. + format: date-time + type: string + resultRef: + description: ResultRef indicates whether the corresponding result + was persisted. + properties: + available: + description: Available indicates whether a result has been + stored for this task + type: boolean + required: + - available + type: object + required: + - attempt + - phase + - recordedAt + type: object executionWorkspace: description: |- ExecutionWorkspace reports the provider-neutral lifecycle state for a requested execution workspace. Provider-native identifiers and credentials are intentionally omitted. properties: + attachedEpoch: + description: AttachedEpoch is the attachment epoch enforced for + this Task. + format: int64 + type: integer + classRef: + description: ClassRef is the provider-neutral class selected for + controller-first execution. + properties: + name: + description: Name is the class name. + minLength: 1 + type: string + required: + - name + type: object cleanupPolicy: description: CleanupPolicy is the resolved cleanup policy. enum: - delete - retain type: string + conditions: + description: Conditions project generic workspace admission, readiness, + attachment, and finalization state. + items: + description: Condition contains details for one aspect of the + current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map density: description: Density reports non-secret actor and worker counts for the workspace provider. @@ -2004,7 +2677,7 @@ spec: type: string type: object provider: - description: Provider is the resolved workspace backend. + description: Provider is the resolved legacy workspace backend. enum: - agent-sandbox - substrate @@ -2042,6 +2715,10 @@ spec: description: Reused reports whether an existing workspace was reattached. type: boolean + state: + description: State is the provider-neutral concrete workspace + state. + type: string templateRef: description: TemplateRef is the resolved workspace template. properties: @@ -2054,46 +2731,19 @@ spec: It defaults to the Task namespace, or the controller namespace when configured. type: string type: object - type: object - harnessRuntime: - description: |- - HarnessRuntime records the controller-resolved harness runtime target for an - in-flight agent turn. It intentionally stores only non-secret routing metadata - and Secret references, never bearer values. - properties: - authRefField: - description: AuthRefField is the Secret data field selected when - the turn started. - type: string - authRefName: - description: AuthRefName is the Secret name selected when the - turn started. - type: string - authRefResourceVersion: - description: AuthRefResourceVersion is the auth Secret resourceVersion - validated before starting the turn. - type: string - contractVersion: - description: ContractVersion is the Orka harness contract version - used for the turn. - type: string - endpoint: - description: Endpoint is the non-secret harness base URL selected - when the turn started. - type: string - runtimeGeneration: - description: RuntimeGeneration is the AgentRuntime generation - selected when the turn started. - format: int64 - type: integer - runtimeName: - description: RuntimeName is the runtime name advertised by the - harness capabilities and sent in turn metadata. - type: string - runtimeRefName: - description: RuntimeRefName is the AgentRuntime name for custom - runtimeRef turns. Empty means built-in CLI wrapper. - type: string + workspaceRef: + description: WorkspaceRef identifies the concrete ExecutionWorkspace + in the Task namespace. + properties: + name: + description: Name is the ExecutionWorkspace name. + type: string + uid: + description: UID is the immutable ExecutionWorkspace UID. + type: string + required: + - name + type: object type: object iteration: description: |- @@ -2123,6 +2773,7 @@ spec: enum: - Pending - Running + - Finalizing - Succeeded - Failed - Scheduled @@ -2147,6 +2798,9 @@ spec: called type: boolean type: object + x-kubernetes-validations: + - message: executionOutcome is immutable once recorded + rule: '!has(oldSelf.executionOutcome) || self.executionOutcome == oldSelf.executionOutcome' type: object served: true storage: true diff --git a/charts/orka/crds/tool-customresourcedefinition.yaml b/charts/orka/crds/tool-customresourcedefinition.yaml index 3ffc6ad71..76a2338a2 100644 --- a/charts/orka/crds/tool-customresourcedefinition.yaml +++ b/charts/orka/crds/tool-customresourcedefinition.yaml @@ -113,6 +113,17 @@ spec: - PATCH - DELETE type: string + outboundAccessPolicyRef: + description: OutboundAccessPolicyRef references an OutboundAccessPolicy + in the same namespace. + properties: + name: + description: Name is the referenced object name. + minLength: 1 + type: string + required: + - name + type: object timeout: description: 'Timeout is the request timeout (default: 30s)' type: string @@ -134,8 +145,8 @@ spec: Defaults to /mcp. type: string substrateActor: - description: SubstrateActor configures a durable Substrate actor - that hosts the MCP server. + description: SubstrateActor configures the legacy durable Substrate + actor backend. properties: boot: description: Boot asks Substrate to boot this actor from scratch @@ -169,9 +180,34 @@ spec: required: - templateRef type: object - required: - - substrateActor + workspace: + description: Workspace configures a provider-neutral Service-mode + ExecutionWorkspace. + properties: + classRef: + description: ClassRef references a class in the Tool namespace. + properties: + name: + description: Name is the class name. + minLength: 1 + type: string + required: + - name + type: object + port: + description: Port is the service port exposed by the MCP server. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - classRef + - port + type: object type: object + x-kubernetes-validations: + - message: exactly one of substrateActor or workspace is required + rule: has(self.substrateActor) != has(self.workspace) parameters: description: Parameters is the JSON Schema for tool parameters (OpenAI function calling format) @@ -180,11 +216,13 @@ spec: - description type: object x-kubernetes-validations: - - message: http or mcp.substrateActor is required - rule: has(self.http) || (has(self.mcp) && has(self.mcp.substrateActor)) - - message: http.url is required unless mcp.substrateActor is set - rule: '!has(self.http) || (has(self.mcp) && has(self.mcp.substrateActor)) - || (has(self.http.url) && self.http.url.size() > 0)' + - message: http or an MCP workspace backend is required + rule: has(self.http) || (has(self.mcp) && (has(self.mcp.substrateActor) + || has(self.mcp.workspace))) + - message: http.url is required unless an MCP workspace backend is set + rule: '!has(self.http) || (has(self.mcp) && (has(self.mcp.substrateActor) + || has(self.mcp.workspace))) || (has(self.http.url) && self.http.url.size() + > 0)' status: description: ToolStatus defines the observed state of Tool properties: @@ -303,6 +341,100 @@ spec: description: LastCheck is the timestamp of the last health check format: date-time type: string + workspace: + description: Workspace reports the provider-neutral Service workspace + backing this Tool. + properties: + classRef: + description: ClassRef is the selected Service workspace class. + properties: + name: + description: Name is the class name. + minLength: 1 + type: string + required: + - name + type: object + conditions: + description: Conditions project generic readiness and cleanup + state. + items: + description: Condition contains details for one aspect of the + current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + state: + description: State is the provider-neutral workspace state. + type: string + workspaceRef: + description: WorkspaceRef identifies the owned concrete workspace. + properties: + name: + description: Name is the ExecutionWorkspace name. + type: string + uid: + description: UID is the immutable ExecutionWorkspace UID. + type: string + required: + - name + type: object + required: + - classRef + type: object required: - available type: object diff --git a/charts/orka/templates/NOTES.txt b/charts/orka/templates/NOTES.txt index b8c0a50b5..06acaeed2 100644 --- a/charts/orka/templates/NOTES.txt +++ b/charts/orka/templates/NOTES.txt @@ -1,10 +1,10 @@ Orka has been installed. -Controller: {{ include "orka.fullname" . }}-controller +Controller: {{ include "orka.controllerName" . }} Namespace: {{ .Release.Namespace }} CRD lifecycle: - - A fresh install creates Orka's twelve cluster-scoped CRDs unless --skip-crds is used. + - A fresh install creates Orka's 26 cluster-scoped CRDs unless --skip-crds is used. - Helm does not update CRDs during helm upgrade. Apply the CRDs from the exact target chart before upgrading the release. - helm uninstall retains the CRDs and all Orka custom resources. @@ -25,7 +25,7 @@ Upgrade guidance: helm show readme {{- else }} -✅ SQLite store is using persistent storage (PVC: {{ include "orka.fullname" . }}-store). +✅ SQLite store is using persistent storage (PVC: {{ include "orka.storeName" . }}). Data will survive pod restarts. {{- end }} diff --git a/charts/orka/templates/_helpers.tpl b/charts/orka/templates/_helpers.tpl index 7764f35bf..c2b1b5d38 100644 --- a/charts/orka/templates/_helpers.tpl +++ b/charts/orka/templates/_helpers.tpl @@ -78,16 +78,85 @@ suffix so long release names cannot collapse all trust tiers to one name. {{- printf "%s-container-worker" (include "orka.fullname" . | trunc 46 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} {{- end }} +{{- define "orka.controllerName" -}} +{{- printf "%s-controller" (include "orka.fullname" . | trunc 52 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.publisherName" -}} +{{- printf "%s-workspace-publisher" (include "orka.fullname" . | trunc 43 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.publisherAuthSecretName" -}} +{{- printf "%s-workspace-publisher-auth" (include "orka.fullname" . | trunc 38 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.acpArtifactSecretName" -}} +{{- printf "%s-acp-artifact-capability" (include "orka.fullname" . | trunc 39 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.providerProxyName" -}} +{{- printf "%s-provider-auth-proxy" (include "orka.fullname" . | trunc 43 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.scmEgressProxyName" -}} +{{- printf "%s-scm-egress-proxy" (include "orka.fullname" . | trunc 46 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.scmEgressProxyAuthSecretName" -}} +{{- printf "%s-scm-egress-proxy-auth" (include "orka.fullname" . | trunc 41 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.storeName" -}} +{{- printf "%s-store" (include "orka.fullname" . | trunc 57 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.vekilIngressPolicyName" -}} +{{- printf "%s-vekil-ingress" (include "orka.fullname" . | trunc 49 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + {{/* -Create release-scoped harness-wrapper names while reserving room for suffixes -that must remain valid DNS labels (notably the Service name). +Create the name of the workspace publisher ServiceAccount to use. */}} -{{- define "orka.harnessWrapperName" -}} -{{- printf "%s-agent-harness-wrapper" (include "orka.fullname" . | trunc 41 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- define "orka.publisherServiceAccountName" -}} +{{- if .Values.publisher.serviceAccount.create }} +{{- default (include "orka.publisherName" .) .Values.publisher.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.publisher.serviceAccount.name }} +{{- end }} {{- end }} -{{- define "orka.harnessWrapperAuthSecretName" -}} -{{- printf "%s-harness-wrapper-auth" (include "orka.fullname" . | trunc 42 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{/* +Reject mutable ACP runtime image references when a provider image is configured. +An empty provider image leaves that provider unavailable; Tasks still fail closed +because the ACP runtime remains enabled and has no legacy fallback. +*/}} +{{- define "orka.validateACPRuntimeImage" -}} +{{- $name := .name -}} +{{- $ref := default "" .ref -}} +{{- if and $ref (not (regexMatch "^.+@sha256:[0-9a-f]{64}$" $ref)) -}} +{{- fail (printf "%s must be an immutable image reference ending in @sha256:<64 lowercase hex characters>; got %q" $name $ref) -}} +{{- end -}} +{{- end }} + +{{/* +The chart-managed provider proxy is release-namespaced and its NetworkPolicies +are intentionally pinned to the chart-supported Vekil Service. +*/}} +{{- define "orka.validateProviderProxyConfig" -}} +{{- if .Values.providerProxy.enabled -}} +{{- $configuredNamespace := trim (default "" .Values.controller.acpRuntime.providerProxyNamespace) -}} +{{- if and $configuredNamespace (ne $configuredNamespace .Release.Namespace) -}} +{{- fail (printf "controller.acpRuntime.providerProxyNamespace must be empty or match the Helm release namespace %q when providerProxy.enabled=true" .Release.Namespace) -}} +{{- end -}} +{{- $upstream := trimSuffix "/" (trim (default "" .Values.providerProxy.upstreamBaseURL)) -}} +{{- if ne $upstream "http://vekil.vekil-system.svc:1337" -}} +{{- fail "providerProxy.upstreamBaseURL must be http://vekil.vekil-system.svc:1337 (an optional trailing slash is accepted)" -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{- define "orka.providerProxyUpstreamBaseURL" -}} +{{- trimSuffix "/" (trim (default "" .Values.providerProxy.upstreamBaseURL)) -}} {{- end }} @@ -135,3 +204,12 @@ Create release-scoped static worker ClusterRoleBinding names. {{- define "orka.containerWorkerClusterRoleBindingName" -}} {{- printf "%s-container-worker-rolebinding" (include "orka.fullname" .) | trunc 253 | trimSuffix "-" }} {{- end }} + +{{/* Render repository@digest when an immutable digest is configured. */}} +{{- define "orka.imageRef" -}} +{{- if .digest -}} +{{ printf "%s@%s" .repository .digest }} +{{- else -}} +{{ printf "%s:%s" .repository .tag }} +{{- end -}} +{{- end }} diff --git a/charts/orka/templates/acp-artifact-secret.yaml b/charts/orka/templates/acp-artifact-secret.yaml new file mode 100644 index 000000000..3a6651398 --- /dev/null +++ b/charts/orka/templates/acp-artifact-secret.yaml @@ -0,0 +1,19 @@ +{{- if not .Values.controller.acpArtifact.existingSecret }} +{{- $secretName := include "orka.acpArtifactSecretName" . }} +{{- $secretKey := .Values.controller.acpArtifact.secretKey | default "capability-secret" }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }} +{{- $existingValue := "" }} +{{- if and $existing (hasKey $existing.data $secretKey) }} +{{- $existingValue = (index $existing.data $secretKey | b64dec) }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: acp-artifact-api +type: Opaque +stringData: + {{ $secretKey }}: {{ default (default (randAlphaNum 64) $existingValue) .Values.controller.acpArtifact.secret | quote }} +{{- end }} diff --git a/charts/orka/templates/deployment.yaml b/charts/orka/templates/deployment.yaml index 13e213d59..7c6246d80 100644 --- a/charts/orka/templates/deployment.yaml +++ b/charts/orka/templates/deployment.yaml @@ -1,3 +1,21 @@ +{{- include "orka.validateProviderProxyConfig" . }} +{{- if .Values.controller.acpRuntime.enabled }} +{{- if not (regexMatch "^sha256:[0-9a-f]{64}$" (.Values.controller.image.digest | default "")) }} +{{- fail "controller.image.digest must be a sha256 digest when controller.acpRuntime.enabled=true" }} +{{- end }} +{{- if or (not .Values.publisher.enabled) (not (regexMatch "^sha256:[0-9a-f]{64}$" (.Values.publisher.image.digest | default ""))) }} +{{- fail "publisher must be enabled with publisher.image.digest set when controller.acpRuntime.enabled=true" }} +{{- end }} +{{- if not .Values.store.persistence.enabled }} +{{- fail "store.persistence.enabled must be true when controller.acpRuntime.enabled=true" }} +{{- end }} +{{- include "orka.validateACPRuntimeImage" (dict "name" "controller.acpRuntime.codexImage" "ref" .Values.controller.acpRuntime.codexImage) }} +{{- include "orka.validateACPRuntimeImage" (dict "name" "controller.acpRuntime.claudeImage" "ref" .Values.controller.acpRuntime.claudeImage) }} +{{- include "orka.validateACPRuntimeImage" (dict "name" "controller.acpRuntime.copilotImage" "ref" .Values.controller.acpRuntime.copilotImage) }} +{{- if and .Values.controller.acpRuntime.enabled (not .Values.providerProxy.enabled) }} +{{- fail "providerProxy.enabled must be true when controller.acpRuntime.enabled is true" }} +{{- end }} +{{- end }} {{- $gatewayCRDsReady := false -}} {{- if and .Values.controller.gateway.enabled (not .Values.controller.gateway.crdsReadyOverride) -}} {{- $gatewayClassCRD := lookup "apiextensions.k8s.io/v1" "CustomResourceDefinition" "" "gatewayclasses.gateway.orka.ai" -}} @@ -17,7 +35,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} labels: {{- include "orka.labels" . | nindent 4 }} app.kubernetes.io/component: controller @@ -36,9 +54,15 @@ spec: labels: {{- include "orka.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: controller - {{- with .Values.annotations }} + orka.ai/network-role: controller + {{- if or .Values.annotations .Values.publisher.auth.rolloutNonce }} annotations: + {{- with .Values.annotations }} {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.publisher.auth.rolloutNonce }} + orka.ai/publisher-auth-rollout-nonce: {{ . | quote }} + {{- end }} {{- end }} spec: serviceAccountName: {{ include "orka.serviceAccountName" . }} @@ -46,9 +70,12 @@ spec: securityContext: {{- toYaml . | nindent 8 }} {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.controller.acpRuntime.upgradeDrain.enabled }} + terminationGracePeriodSeconds: {{ .Values.controller.acpRuntime.upgradeDrain.terminationGracePeriodSeconds }} + {{- end }} containers: - name: controller - image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag }}" + image: {{ include "orka.imageRef" .Values.controller.image | quote }} imagePullPolicy: {{ .Values.controller.image.pullPolicy }} args: - --api-port={{ .Values.controller.apiPort }} @@ -60,6 +87,30 @@ spec: - --watch-namespace={{ .Values.controller.watchNamespace }} {{- end }} - --leader-elect={{ .Values.controller.leaderElect }} + - --acp-runtime-enabled={{ .Values.controller.acpRuntime.enabled }} + - --acp-runtime-namespace={{ .Values.controller.acpRuntime.namespace }} + {{- if and .Values.controller.acpRuntime.enabled .Values.controller.acpRuntime.upgradeDrain.enabled }} + - --acp-upgrade-drain-bind-address=127.0.0.1:{{ .Values.controller.acpRuntime.upgradeDrain.port }} + - --acp-upgrade-drain-timeout={{ .Values.controller.acpRuntime.upgradeDrain.timeout }} + - --acp-upgrade-drain-poll-interval={{ .Values.controller.acpRuntime.upgradeDrain.pollInterval }} + - --acp-upgrade-drain-trigger-timeout={{ .Values.controller.acpRuntime.upgradeDrain.triggerTimeout }} + - --acp-upgrade-drain-marker-namespace={{ .Release.Namespace }} + {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.providerProxy.enabled }} + - --acp-provider-proxy-base-url=http://{{ include "orka.providerProxyName" . }}.{{ .Release.Namespace }}.svc:8080 + - --acp-provider-proxy-namespace={{ .Release.Namespace }} + - --acp-provider-proxy-pod-labels=orka.ai/network-role=provider-auth-proxy + - --acp-provider-proxy-token-file=/var/run/orka/provider-auth/token + {{- end }} + {{- if .Values.controller.acpRuntime.codexImage }} + - {{ printf "--acp-codex-runtime-image=%s" .Values.controller.acpRuntime.codexImage | quote }} + {{- end }} + {{- if .Values.controller.acpRuntime.claudeImage }} + - {{ printf "--acp-claude-runtime-image=%s" .Values.controller.acpRuntime.claudeImage | quote }} + {{- end }} + {{- if .Values.controller.acpRuntime.copilotImage }} + - {{ printf "--acp-copilot-runtime-image=%s" .Values.controller.acpRuntime.copilotImage | quote }} + {{- end }} - --zap-log-level={{ .Values.controller.logLevel }} - --gateway-enabled={{ $gatewayRuntimeEnabled }} - --gateway-pending-per-session={{ .Values.controller.gateway.pendingPerSession }} @@ -282,12 +333,20 @@ spec: value: {{ include "orka.vendorWorkerClusterRoleName" . | quote }} - name: ORKA_CONTAINER_WORKER_CLUSTER_ROLE_NAME value: {{ include "orka.containerWorkerClusterRoleName" . | quote }} - - name: ORKA_HARNESS_WRAPPER_ENDPOINT - value: http://{{ include "orka.harnessWrapperName" . }}:8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_HARNESS_WRAPPER_SERVICE_ACCOUNT_NAME - value: {{ include "orka.harnessWrapperName" . | quote }} + - name: ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE + value: /var/run/orka/acp-artifacts/capability-secret + - name: ORKA_ACP_ARTIFACT_ROOT + value: /data/acp-artifacts + - name: ORKA_ACP_ARTIFACT_MAX_BYTES + value: {{ .Values.controller.acpArtifact.maxBytes | quote }} + {{- if .Values.publisher.enabled }} + - name: ORKA_WORKSPACE_PUBLISHER_URL + value: http://{{ include "orka.publisherName" . }}:8080 + - name: ORKA_WORKSPACE_PUBLISHER_CONTROLLER_TOKEN_FILE + value: /var/run/orka/publisher-auth/controller-token + - name: ORKA_WORKSPACE_PUBLISHER_CAPABILITY_SECRET_FILE + value: /var/run/orka/publisher-auth/operation-capability-secret + {{- end }} {{- if .Values.github.webhook.secretName }} - name: ORKA_GITHUB_WEBHOOK_SECRET valueFrom: @@ -335,6 +394,15 @@ spec: - name: health containerPort: {{ .Values.controller.healthPort }} protocol: TCP + {{- if and .Values.controller.acpRuntime.enabled .Values.controller.acpRuntime.upgradeDrain.enabled }} + lifecycle: + preStop: + exec: + command: + - /manager + - {{ printf "--acp-upgrade-drain-trigger-url=http://127.0.0.1:%v%s" .Values.controller.acpRuntime.upgradeDrain.port "/acp/upgrade-drain" | quote }} + - {{ printf "--acp-upgrade-drain-trigger-timeout=%s" .Values.controller.acpRuntime.upgradeDrain.triggerTimeout | quote }} + {{- end }} livenessProbe: httpGet: path: /healthz @@ -360,25 +428,56 @@ spec: mountPath: /tmp - name: store mountPath: /data - - name: harness-wrapper-auth - mountPath: /var/run/orka/harness-wrapper + - name: acp-artifact-capability + mountPath: /var/run/orka/acp-artifacts + readOnly: true + {{- if .Values.publisher.enabled }} + - name: workspace-publisher-auth + mountPath: /var/run/orka/publisher-auth + readOnly: true + {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.providerProxy.enabled }} + - name: provider-auth-proxy + mountPath: /var/run/orka/provider-auth readOnly: true + {{- end }} volumes: - name: tmp emptyDir: {} - - name: harness-wrapper-auth + - name: acp-artifact-capability secret: - secretName: {{ .Values.workers.harnessWrapper.auth.existingSecret | default (include "orka.harnessWrapperAuthSecretName" .) }} + secretName: {{ .Values.controller.acpArtifact.existingSecret | default (include "orka.acpArtifactSecretName" .) }} + defaultMode: 0400 items: - - key: {{ .Values.workers.harnessWrapper.auth.tokenKey | default "token" }} - path: token + - key: {{ .Values.controller.acpArtifact.secretKey | default "capability-secret" }} + path: capability-secret - name: store {{- if .Values.store.persistence.enabled }} persistentVolumeClaim: - claimName: {{ include "orka.fullname" . }}-store + claimName: {{ include "orka.storeName" . }} {{- else }} emptyDir: {} {{- end }} + {{- if .Values.publisher.enabled }} + - name: workspace-publisher-auth + secret: + secretName: {{ .Values.publisher.auth.existingSecret | default (include "orka.publisherAuthSecretName" .) }} + defaultMode: 0400 + items: + - key: {{ .Values.publisher.auth.controllerTokenKey | default "controller-token" }} + path: controller-token + - key: {{ .Values.publisher.auth.capabilitySecretKey | default "operation-capability-secret" }} + path: operation-capability-secret + {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.providerProxy.enabled }} + - name: provider-auth-proxy + secret: + secretName: {{ .Values.providerProxy.auth.existingSecret | default (include "orka.providerProxyName" .) }} + defaultMode: 0400 + items: + - key: {{ .Values.providerProxy.auth.tokenKey | default "token" }} + path: token + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/orka/templates/harness-wrapper-deployment.yaml b/charts/orka/templates/harness-wrapper-deployment.yaml deleted file mode 100644 index 7d42a6b64..000000000 --- a/charts/orka/templates/harness-wrapper-deployment.yaml +++ /dev/null @@ -1,110 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "orka.harnessWrapperName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper - template: - metadata: - labels: - {{- include "orka.selectorLabels" . | nindent 8 }} - app.kubernetes.io/component: agent-harness-wrapper - spec: - serviceAccountName: {{ include "orka.harnessWrapperName" . }} - automountServiceAccountToken: false - securityContext: - runAsUser: 0 - runAsGroup: 0 - seccompProfile: - type: RuntimeDefault - containers: - - name: wrapper - image: {{ .Values.workers.harnessWrapper.image.repository }}:{{ .Values.workers.harnessWrapper.image.tag }} - imagePullPolicy: {{ .Values.workers.harnessWrapper.image.pullPolicy }} - ports: - - name: http - containerPort: 8080 - protocol: TCP - env: - - name: ORKA_HARNESS_WRAPPER_RUNTIME - value: multi - - name: ORKA_HARNESS_WRAPPER_LISTEN_ADDR - value: :8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_ALLOW_BASH - value: "true" - - name: ORKA_HARNESS_WRAPPER_CHILD_UID - value: "1000" - - name: ORKA_HARNESS_WRAPPER_CHILD_GID - value: "1000" - {{- if .Values.workers.harnessWrapper.codexSandboxMode }} - - name: ORKA_CODEX_SANDBOX_MODE - value: {{ .Values.workers.harnessWrapper.codexSandboxMode | quote }} - {{- end }} - - name: ORKA_SA_TOKEN_PATH - value: /var/run/orka/upload-token/token - volumeMounts: - - name: auth - mountPath: /var/run/orka/harness-wrapper - readOnly: true - - name: upload-token - mountPath: /var/run/orka/upload-token - readOnly: true - - name: tmp - mountPath: /tmp - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsUser: 0 - runAsGroup: 0 - capabilities: - drop: - - ALL - add: - - SETUID - - SETGID - - CHOWN - - KILL - - FOWNER - livenessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 10 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.workers.harnessWrapper.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: upload-token - projected: - defaultMode: 0400 - sources: - - serviceAccountToken: - path: token - - name: auth - secret: - secretName: {{ .Values.workers.harnessWrapper.auth.existingSecret | default (include "orka.harnessWrapperAuthSecretName" .) }} - defaultMode: 0400 - items: - - key: {{ .Values.workers.harnessWrapper.auth.tokenKey | default "token" }} - path: token - - name: tmp - emptyDir: {} diff --git a/charts/orka/templates/harness-wrapper-secret.yaml b/charts/orka/templates/harness-wrapper-secret.yaml deleted file mode 100644 index 4a3a5c968..000000000 --- a/charts/orka/templates/harness-wrapper-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if not .Values.workers.harnessWrapper.auth.existingSecret }} -{{- $secretName := include "orka.harnessWrapperAuthSecretName" . }} -{{- $tokenKey := .Values.workers.harnessWrapper.auth.tokenKey | default "token" }} -{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }} -{{- $existingToken := "" }} -{{- if and $existing (hasKey $existing.data $tokenKey) }} -{{- $existingToken = (index $existing.data $tokenKey | b64dec) }} -{{- end }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ $secretName }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper -type: Opaque -stringData: - {{ $tokenKey }}: {{ default (default (randAlphaNum 32) $existingToken) .Values.workers.harnessWrapper.auth.token | quote }} -{{- end }} diff --git a/charts/orka/templates/provider-proxy-deployment.yaml b/charts/orka/templates/provider-proxy-deployment.yaml new file mode 100644 index 000000000..c3ab9e123 --- /dev/null +++ b/charts/orka/templates/provider-proxy-deployment.yaml @@ -0,0 +1,82 @@ +{{- include "orka.validateProviderProxyConfig" . }} +{{- if .Values.providerProxy.enabled }} +{{- $secretName := .Values.providerProxy.auth.existingSecret | default (include "orka.providerProxyName" .) }} +{{- $currentKey := .Values.providerProxy.auth.tokenKey | default "token" }} +{{- $previousKey := .Values.providerProxy.auth.previousTokenKey | default "previous-token" }} +{{- $previousValidUntilKey := .Values.providerProxy.auth.previousTokenValidUntilKey | default "previous-token-valid-until" }} +{{- if or (eq $currentKey $previousKey) (eq $currentKey $previousValidUntilKey) (eq $previousKey $previousValidUntilKey) }} +{{- fail "providerProxy auth token keys must differ" }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +spec: + replicas: 1 + strategy: {type: Recreate} + selector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: provider-auth-proxy + template: + metadata: + {{- with .Values.providerProxy.rolloutNonce }} + annotations: + orka.ai/provider-auth-rollout-nonce: {{ . | quote }} + {{- end }} + labels: + {{- include "orka.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: provider-auth-proxy + orka.ai/network-role: provider-auth-proxy + spec: + serviceAccountName: {{ include "orka.providerProxyName" . }} + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: {type: RuntimeDefault} + containers: + - name: proxy + image: {{ include "orka.imageRef" .Values.controller.image | quote }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + command: [/provider-auth-proxy] + args: + - --listen-address=:8080 + - {{ printf "--upstream-base-url=%s" (include "orka.providerProxyUpstreamBaseURL" .) | quote }} + - --token-file=/var/run/secrets/orka/provider-auth/token + - --previous-token-file=/var/run/secrets/orka/provider-auth/previous-token + - --previous-token-valid-until-file=/var/run/secrets/orka/provider-auth/previous-token-valid-until + - {{ printf "--token-reload-interval=%s" (.Values.providerProxy.tokenReloadInterval | default "5s") | quote }} + - {{ printf "--previous-token-overlap=%s" (.Values.providerProxy.previousTokenOverlap | default "10m") | quote }} + ports: [{name: http, containerPort: 8080}] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: + {{- toYaml .Values.providerProxy.resources | nindent 12 }} + readinessProbe: {httpGet: {path: /readyz, port: http}} + livenessProbe: {httpGet: {path: /healthz, port: http}} + volumeMounts: + - {name: provider-auth, mountPath: /var/run/secrets/orka/provider-auth, readOnly: true} + volumes: + - name: provider-auth + projected: + defaultMode: 0440 + sources: + - secret: + name: {{ $secretName }} + optional: true + items: + - key: {{ $currentKey }} + path: token + - key: {{ $previousKey }} + path: previous-token + - key: {{ $previousValidUntilKey }} + path: previous-token-valid-until +{{- end }} diff --git a/charts/orka/templates/provider-proxy-networkpolicy.yaml b/charts/orka/templates/provider-proxy-networkpolicy.yaml new file mode 100644 index 000000000..fabca2ebe --- /dev/null +++ b/charts/orka/templates/provider-proxy-networkpolicy.yaml @@ -0,0 +1,36 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: provider-auth-proxy + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.controller.acpRuntime.namespace }} + podSelector: + matchLabels: + orka.ai/network-role: provider-client + ports: [{protocol: TCP, port: 8080}] + egress: + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kube-system} + podSelector: + matchLabels: {k8s-app: kube-dns} + ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}] + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: vekil-system} + podSelector: + matchLabels: {app.kubernetes.io/name: vekil} + ports: [{protocol: TCP, port: 1337}] +{{- end }} diff --git a/charts/orka/templates/provider-proxy-secret.yaml b/charts/orka/templates/provider-proxy-secret.yaml new file mode 100644 index 000000000..068115022 --- /dev/null +++ b/charts/orka/templates/provider-proxy-secret.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.providerProxy.enabled (not .Values.providerProxy.auth.existingSecret) }} +{{- $name := include "orka.providerProxyName" . }} +{{- $key := .Values.providerProxy.auth.tokenKey | default "token" }} +{{- $previousKey := .Values.providerProxy.auth.previousTokenKey | default "previous-token" }} +{{- $previousValidUntilKey := .Values.providerProxy.auth.previousTokenValidUntilKey | default "previous-token-valid-until" }} +{{- $previousToken := .Values.providerProxy.auth.previousToken | default "" }} +{{- $previousValidUntil := .Values.providerProxy.auth.previousTokenValidUntil | default "" }} +{{- if or (eq $key $previousKey) (eq $key $previousValidUntilKey) (eq $previousKey $previousValidUntilKey) }} +{{- fail "providerProxy auth token keys must differ" }} +{{- end }} +{{- if ne (empty $previousToken) (empty $previousValidUntil) }} +{{- fail "providerProxy.auth.previousToken and previousTokenValidUntil must be set together" }} +{{- end }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name }} +{{- $existingValue := "" }} +{{- if and $existing (hasKey $existing.data $key) }}{{- $existingValue = (index $existing.data $key | b64dec) }}{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +type: Opaque +data: + {{ $key }}: {{ default (default (randAlphaNum 64) $existingValue) .Values.providerProxy.auth.token | toString | b64enc | quote }} + {{- if $previousToken }} + {{ $previousKey }}: {{ $previousToken | toString | b64enc | quote }} + {{ $previousValidUntilKey }}: {{ $previousValidUntil | toString | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/charts/orka/templates/provider-proxy-service.yaml b/charts/orka/templates/provider-proxy-service.yaml new file mode 100644 index 000000000..f79e37bbb --- /dev/null +++ b/charts/orka/templates/provider-proxy-service.yaml @@ -0,0 +1,15 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +spec: + selector: + {{- include "orka.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy + ports: + - {name: http, port: 8080, targetPort: http} +{{- end }} diff --git a/charts/orka/templates/provider-proxy-serviceaccount.yaml b/charts/orka/templates/provider-proxy-serviceaccount.yaml new file mode 100644 index 000000000..b066ee20e --- /dev/null +++ b/charts/orka/templates/provider-proxy-serviceaccount.yaml @@ -0,0 +1,10 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +automountServiceAccountToken: false +{{- end }} diff --git a/charts/orka/templates/publisher-deployment.yaml b/charts/orka/templates/publisher-deployment.yaml new file mode 100644 index 000000000..07d7d2005 --- /dev/null +++ b/charts/orka/templates/publisher-deployment.yaml @@ -0,0 +1,113 @@ +{{- if and .Values.publisher.enabled (not .Values.scmEgressProxy.enabled) }} +{{- fail "publisher.enabled requires scmEgressProxy.enabled so public HTTPS egress remains proxy-only" }} +{{- end }} +{{- if .Values.publisher.enabled }} +{{- $scmProxySecretName := .Values.scmEgressProxy.auth.existingSecret | default (include "orka.scmEgressProxyAuthSecretName" .) }} +{{- $scmProxyTokenKey := .Values.scmEgressProxy.auth.tokenKey | default "token" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "orka.publisherName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher +spec: + replicas: 1 + strategy: {type: Recreate} + selector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: workspace-publisher + template: + metadata: + {{- if or .Values.publisher.auth.rolloutNonce .Values.scmEgressProxy.auth.rolloutNonce }} + annotations: + {{- with .Values.publisher.auth.rolloutNonce }} + orka.ai/publisher-auth-rollout-nonce: {{ . | quote }} + {{- end }} + {{- with .Values.scmEgressProxy.auth.rolloutNonce }} + orka.ai/scm-egress-proxy-auth-rollout-nonce: {{ . | quote }} + {{- end }} + {{- end }} + labels: + {{- include "orka.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: workspace-publisher + orka.ai/network-role: workspace-publisher + spec: + serviceAccountName: {{ include "orka.publisherServiceAccountName" . }} + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: {type: RuntimeDefault} + containers: + - name: publisher + image: {{ include "orka.imageRef" .Values.publisher.image | quote }} + imagePullPolicy: {{ .Values.publisher.image.pullPolicy }} + ports: + - {name: http, containerPort: 8080} + env: + - name: ORKA_SCM_EGRESS_PROXY_TOKEN + valueFrom: + secretKeyRef: + name: {{ $scmProxySecretName }} + key: {{ $scmProxyTokenKey }} + - name: HTTPS_PROXY + value: {{ printf "http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@%s.%s.svc:%v" (include "orka.scmEgressProxyName" .) .Release.Namespace 8080 | quote }} + - name: https_proxy + value: {{ printf "http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@%s.%s.svc:%v" (include "orka.scmEgressProxyName" .) .Release.Namespace 8080 | quote }} + - {name: NO_PROXY, value: {{ .Values.scmEgressProxy.noProxy | quote }}} + - {name: no_proxy, value: {{ .Values.scmEgressProxy.noProxy | quote }}} + - {name: ORKA_PUBLISHER_SCM_EGRESS_PROXY_REQUIRED, value: "true"} + - {name: ORKA_PUBLISHER_LISTEN_ADDRESS, value: ":8080"} + - {name: ORKA_PUBLISHER_TEMP_ROOT, value: /tmp/orka-workspace-publisher/runtime} + - {name: ORKA_PUBLISHER_CONTROLLER_TOKEN_FILE, value: /var/run/orka/publisher-auth/controller-token} + - {name: ORKA_PUBLISHER_OPERATION_CAPABILITY_SECRET_FILE, value: /var/run/orka/publisher-auth/operation-capability-secret} + - name: ORKA_PUBLISHER_ARTIFACT_AUTHORIZATION_BROKER_URL + value: http://{{ include "orka.fullname" . }}:{{ .Values.service.port }} + - name: ORKA_PUBLISHER_ARTIFACT_API_URL + value: http://{{ include "orka.fullname" . }}:{{ .Values.service.port }} + - name: ORKA_PUBLISHER_CREDENTIAL_BROKER_URL + value: http://{{ include "orka.fullname" . }}:{{ .Values.service.port }} + - {name: ORKA_PUBLISHER_ALLOWED_SCM_HOSTS, value: {{ .Values.publisher.allowedSCMHosts | quote }}} + - {name: ORKA_PUBLISHER_GITHUB_PR_ENABLED, value: {{ .Values.publisher.githubPR.enabled | quote }}} + {{- if .Values.publisher.githubPR.enabled }} + - {name: ORKA_PUBLISHER_GITHUB_API_BASE_URL, value: {{ .Values.publisher.githubPR.apiBaseURL | quote }}} + - {name: ORKA_PUBLISHER_GITHUB_REQUEST_TIMEOUT, value: {{ .Values.publisher.githubPR.requestTimeout | quote }}} + - {name: ORKA_PUBLISHER_GITHUB_MAX_RESPONSE_BYTES, value: {{ .Values.publisher.githubPR.maxResponseBytes | quote }}} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: + {{- toYaml .Values.publisher.resources | nindent 12 }} + volumeMounts: + - {name: data, mountPath: /data} + - {name: tmp, mountPath: /tmp/orka-workspace-publisher} + - {name: publisher-auth, mountPath: /var/run/orka/publisher-auth/controller-token, subPath: controller-token, readOnly: true} + - {name: publisher-auth, mountPath: /var/run/orka/publisher-auth/operation-capability-secret, subPath: operation-capability-secret, readOnly: true} + readinessProbe: + httpGet: {path: /v1/health, port: http} + livenessProbe: + httpGet: {path: /v1/health, port: http} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "orka.publisherName" . }} + - name: tmp + emptyDir: {sizeLimit: 1Gi} + - name: publisher-auth + secret: + secretName: {{ .Values.publisher.auth.existingSecret | default (include "orka.publisherAuthSecretName" .) }} + # subPath bind mounts expose regular files to the fail-closed + # publisher loader; fsGroup grants only the Pod group read access. + defaultMode: 0440 + items: + - key: {{ .Values.publisher.auth.controllerTokenKey | default "controller-token" }} + path: controller-token + - key: {{ .Values.publisher.auth.capabilitySecretKey | default "operation-capability-secret" }} + path: operation-capability-secret +{{- end }} diff --git a/charts/orka/templates/publisher-networkpolicy.yaml b/charts/orka/templates/publisher-networkpolicy.yaml new file mode 100644 index 000000000..281274ea8 --- /dev/null +++ b/charts/orka/templates/publisher-networkpolicy.yaml @@ -0,0 +1,40 @@ +{{- if .Values.publisher.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.publisherName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: workspace-publisher + policyTypes: [Ingress, Egress] + ingress: + - from: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: controller + ports: [{protocol: TCP, port: 8080}] + egress: + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kube-system} + podSelector: + matchLabels: {k8s-app: kube-dns} + ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}] + - to: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: controller + ports: [{protocol: TCP, port: {{ .Values.controller.apiPort }}}] + - to: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: scm-egress-proxy + ports: [{protocol: TCP, port: 8080}] +{{- end }} diff --git a/charts/orka/templates/publisher-pvc.yaml b/charts/orka/templates/publisher-pvc.yaml new file mode 100644 index 000000000..25417e82b --- /dev/null +++ b/charts/orka/templates/publisher-pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.publisher.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "orka.publisherName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher +spec: + accessModes: [ReadWriteOnce] + {{- if .Values.publisher.persistence.storageClass }} + storageClassName: {{ .Values.publisher.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.publisher.persistence.size }} +{{- end }} diff --git a/charts/orka/templates/publisher-secret.yaml b/charts/orka/templates/publisher-secret.yaml new file mode 100644 index 000000000..64e1403de --- /dev/null +++ b/charts/orka/templates/publisher-secret.yaml @@ -0,0 +1,23 @@ +{{- if and .Values.publisher.enabled (not .Values.publisher.auth.existingSecret) }} +{{- $name := include "orka.publisherAuthSecretName" . }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name }} +{{- $controllerKey := .Values.publisher.auth.controllerTokenKey | default "controller-token" }} +{{- $capabilityKey := .Values.publisher.auth.capabilitySecretKey | default "operation-capability-secret" }} +{{- $existingController := "" }} +{{- $existingCapability := "" }} +{{- if $existing }} +{{- if hasKey $existing.data $controllerKey }}{{- $existingController = (index $existing.data $controllerKey | b64dec) }}{{- end }} +{{- if hasKey $existing.data $capabilityKey }}{{- $existingCapability = (index $existing.data $capabilityKey | b64dec) }}{{- end }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher +type: Opaque +stringData: + {{ $controllerKey }}: {{ default (default (randAlphaNum 64) $existingController) .Values.publisher.auth.controllerToken | quote }} + {{ $capabilityKey }}: {{ default (default (randAlphaNum 64) $existingCapability) .Values.publisher.auth.capabilitySecret | quote }} +{{- end }} diff --git a/cmd/build/helmify/static/templates/harness-wrapper-service.yaml b/charts/orka/templates/publisher-service.yaml similarity index 54% rename from cmd/build/helmify/static/templates/harness-wrapper-service.yaml rename to charts/orka/templates/publisher-service.yaml index e6d5ddf10..9b18963a1 100644 --- a/cmd/build/helmify/static/templates/harness-wrapper-service.yaml +++ b/charts/orka/templates/publisher-service.yaml @@ -1,16 +1,17 @@ +{{- if .Values.publisher.enabled }} apiVersion: v1 kind: Service metadata: - name: {{ include "orka.harnessWrapperName" . }} + name: {{ include "orka.publisherName" . }} labels: {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper + app.kubernetes.io/component: workspace-publisher spec: selector: {{- include "orka.selectorLabels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper + app.kubernetes.io/component: workspace-publisher ports: - name: http port: 8080 targetPort: http - protocol: TCP +{{- end }} diff --git a/charts/orka/templates/rbac.yaml b/charts/orka/templates/rbac.yaml index ce98dd14e..b218acf5a 100644 --- a/charts/orka/templates/rbac.yaml +++ b/charts/orka/templates/rbac.yaml @@ -3,19 +3,19 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} labels: {{- include "orka.labels" . | nindent 4 }} rules: # Custom Resource permissions - apiGroups: ["core.orka.ai"] - resources: ["tasks", "tools", "agents", "agentruntimes", "providers", "skills", "repositorymonitors", "repositoryscans", "substrateactorpools"] + resources: ["tasks", "tools", "agents", "agentruntimes", "runtimepools", "promptattempts", "runtimesessioncontrols", "branchclaims", "publications", "controllerepochs", "externaleffects", "providers", "skills", "repositorymonitors", "repositoryscans", "substrateactorpools"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["core.orka.ai"] - resources: ["tasks/status", "tools/status", "agents/status", "agentruntimes/status", "providers/status", "skills/status", "repositorymonitors/status", "repositoryscans/status", "substrateactorpools/status"] + resources: ["tasks/status", "tools/status", "agents/status", "agentruntimes/status", "runtimepools/status", "promptattempts/status", "runtimesessioncontrols/status", "branchclaims/status", "publications/status", "controllerepochs/status", "externaleffects/status", "providers/status", "skills/status", "repositorymonitors/status", "repositoryscans/status", "substrateactorpools/status"] verbs: ["get", "update", "patch"] - apiGroups: ["core.orka.ai"] - resources: ["tasks/finalizers", "tools/finalizers", "agents/finalizers", "agentruntimes/finalizers", "providers/finalizers", "skills/finalizers", "repositorymonitors/finalizers", "repositoryscans/finalizers", "substrateactorpools/finalizers"] + resources: ["tasks/finalizers", "tools/finalizers", "agents/finalizers", "agentruntimes/finalizers", "runtimepools/finalizers", "promptattempts/finalizers", "runtimesessioncontrols/finalizers", "branchclaims/finalizers", "publications/finalizers", "controllerepochs/finalizers", "externaleffects/finalizers", "providers/finalizers", "skills/finalizers", "repositorymonitors/finalizers", "repositoryscans/finalizers"] verbs: ["update"] - apiGroups: ["gateway.orka.ai"] resources: ["gatewayclasses", "gateways", "gatewaybindings"] @@ -45,10 +45,10 @@ rules: verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["secrets"] - verbs: ["get", "list", "watch", "create", "update", "delete"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["pods"] - verbs: ["get", "list", "watch"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] @@ -58,15 +58,21 @@ rules: - apiGroups: [""] resources: ["serviceaccounts"] verbs: ["get", "list", "watch", "create", "update"] + - apiGroups: [""] + resources: ["serviceaccounts/token"] + verbs: ["create"] - apiGroups: [""] resources: ["namespaces"] - verbs: ["get", "list", "watch"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch", "create", "patch"] - apiGroups: [""] - resources: ["nodes", "services", "endpoints"] + resources: ["nodes", "endpoints"] verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["discovery.k8s.io"] resources: ["endpointslices"] verbs: ["get", "list", "watch"] @@ -76,14 +82,17 @@ rules: # Workload resource permissions (read-only, for chat K8s tools) - apiGroups: ["apps"] - resources: ["deployments", "replicasets", "statefulsets", "daemonsets"] + resources: ["deployments", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["apps"] + resources: ["statefulsets", "daemonsets"] verbs: ["get", "list", "watch"] - apiGroups: ["networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch"] - apiGroups: ["networking.k8s.io"] resources: ["networkpolicies"] - verbs: ["get", "list", "watch", "create", "delete"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Agent sandbox workspace backend: create/reattach/delete claims and connect to sandbox pods - apiGroups: ["extensions.agents.x-k8s.io"] resources: ["sandboxclaims"] @@ -106,7 +115,7 @@ rules: verbs: ["get", "list", "watch"] - apiGroups: ["policy"] resources: ["poddisruptionbudgets"] - verbs: ["get", "list", "watch"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] verbs: ["get", "list", "watch"] @@ -152,13 +161,13 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} labels: {{- include "orka.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} subjects: - kind: ServiceAccount name: {{ include "orka.serviceAccountName" . }} diff --git a/charts/orka/templates/runtime-namespace.yaml b/charts/orka/templates/runtime-namespace.yaml new file mode 100644 index 000000000..db0051ef4 --- /dev/null +++ b/charts/orka/templates/runtime-namespace.yaml @@ -0,0 +1,13 @@ +{{- if .Values.controller.acpRuntime.enabled }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Values.controller.acpRuntime.namespace }} + labels: + app.kubernetes.io/name: {{ include "orka.name" . }} + app.kubernetes.io/component: acp-runtime + app.kubernetes.io/managed-by: {{ .Release.Service }} + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/audit: restricted +{{- end }} diff --git a/charts/orka/templates/scm-egress-proxy-deployment.yaml b/charts/orka/templates/scm-egress-proxy-deployment.yaml new file mode 100644 index 000000000..5451575af --- /dev/null +++ b/charts/orka/templates/scm-egress-proxy-deployment.yaml @@ -0,0 +1,85 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +{{- $secretName := .Values.scmEgressProxy.auth.existingSecret | default (include "orka.scmEgressProxyAuthSecretName" .) }} +{{- $tokenKey := .Values.scmEgressProxy.auth.tokenKey | default "token" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +spec: + replicas: 1 + strategy: {type: Recreate} + selector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: scm-egress-proxy + template: + metadata: + {{- with .Values.scmEgressProxy.auth.rolloutNonce }} + annotations: + orka.ai/scm-egress-proxy-auth-rollout-nonce: {{ . | quote }} + {{- end }} + labels: + {{- include "orka.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: scm-egress-proxy + orka.ai/network-role: scm-egress-proxy + spec: + serviceAccountName: {{ include "orka.scmEgressProxyName" . }} + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: {type: RuntimeDefault} + containers: + - name: proxy + image: {{ include "orka.imageRef" .Values.controller.image | quote }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + command: [/scm-egress-proxy] + args: + - --listen-address=:8080 + - {{ printf "--allowed-hosts=%s" .Values.publisher.allowedSCMHosts | quote }} + {{- if .Values.publisher.githubPR.enabled }} + - {{ printf "--forge-api-base-url=%s" .Values.publisher.githubPR.apiBaseURL | quote }} + {{- else }} + - --forge-api-base-url= + {{- end }} + - --token-file=/var/run/secrets/orka/scm-egress/token + - {{ printf "--max-request-header-bytes=%v" .Values.scmEgressProxy.maxRequestHeaderBytes | quote }} + - {{ printf "--max-response-header-bytes=%v" .Values.scmEgressProxy.maxResponseHeaderBytes | quote }} + - {{ printf "--max-request-bytes=%v" .Values.scmEgressProxy.maxRequestBytes | quote }} + - {{ printf "--max-response-bytes=%v" .Values.scmEgressProxy.maxResponseBytes | quote }} + - {{ printf "--max-tunnel-bytes=%v" .Values.scmEgressProxy.maxTunnelBytes | quote }} + - {{ printf "--max-concurrent=%v" .Values.scmEgressProxy.maxConcurrent | quote }} + - {{ printf "--resolution-timeout=%s" .Values.scmEgressProxy.resolutionTimeout | quote }} + - {{ printf "--connect-timeout=%s" .Values.scmEgressProxy.connectTimeout | quote }} + - {{ printf "--response-header-timeout=%s" .Values.scmEgressProxy.responseHeaderTimeout | quote }} + - {{ printf "--forward-timeout=%s" .Values.scmEgressProxy.forwardTimeout | quote }} + - {{ printf "--idle-timeout=%s" .Values.scmEgressProxy.idleTimeout | quote }} + - {{ printf "--tunnel-timeout=%s" .Values.scmEgressProxy.tunnelTimeout | quote }} + ports: + - {name: http-proxy, containerPort: 8080} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: + {{- toYaml .Values.scmEgressProxy.resources | nindent 12 }} + readinessProbe: + httpGet: {path: /readyz, port: http-proxy} + livenessProbe: + httpGet: {path: /healthz, port: http-proxy} + volumeMounts: + - {name: auth, mountPath: /var/run/secrets/orka/scm-egress/token, subPath: token, readOnly: true} + volumes: + - name: auth + secret: + secretName: {{ $secretName }} + defaultMode: 0440 + items: + - key: {{ $tokenKey }} + path: token +{{- end }} diff --git a/charts/orka/templates/scm-egress-proxy-networkpolicy.yaml b/charts/orka/templates/scm-egress-proxy-networkpolicy.yaml new file mode 100644 index 000000000..7050e6aa4 --- /dev/null +++ b/charts/orka/templates/scm-egress-proxy-networkpolicy.yaml @@ -0,0 +1,63 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: scm-egress-proxy + policyTypes: [Ingress, Egress] + ingress: + - from: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: workspace-publisher + ports: [{protocol: TCP, port: 8080}] + egress: + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kube-system} + podSelector: + matchLabels: {k8s-app: kube-dns} + ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}] + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 0.0.0.0/8 + - 10.0.0.0/8 + - 100.64.0.0/10 + - 127.0.0.0/8 + - 169.254.0.0/16 + - 172.16.0.0/12 + - 192.0.0.0/24 + - 192.0.2.0/24 + - 192.168.0.0/16 + - 198.18.0.0/15 + - 198.51.100.0/24 + - 203.0.113.0/24 + - 224.0.0.0/4 + - 240.0.0.0/4 + ports: [{protocol: TCP, port: 443}] + - to: + - ipBlock: + cidr: ::/0 + except: + - ::/128 + - ::1/128 + - 64:ff9b::/96 + - 64:ff9b:1::/48 + - 100::/64 + - 2001::/32 + - 2001:db8::/32 + - 2002::/16 + - fc00::/7 + - fe80::/10 + - ff00::/8 + ports: [{protocol: TCP, port: 443}] +{{- end }} diff --git a/charts/orka/templates/scm-egress-proxy-secret.yaml b/charts/orka/templates/scm-egress-proxy-secret.yaml new file mode 100644 index 000000000..e27e9630a --- /dev/null +++ b/charts/orka/templates/scm-egress-proxy-secret.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled (not .Values.scmEgressProxy.auth.existingSecret) }} +{{- $name := include "orka.scmEgressProxyAuthSecretName" . }} +{{- $key := .Values.scmEgressProxy.auth.tokenKey | default "token" }} +{{- $explicit := .Values.scmEgressProxy.auth.token | default "" | toString }} +{{- if and $explicit (not (regexMatch "^[A-Za-z0-9._~-]{32,256}$" $explicit)) }} +{{- fail "scmEgressProxy.auth.token must contain 32-256 RFC 3986 unreserved characters" }} +{{- end }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name }} +{{- $existingValue := "" }} +{{- if and $existing (hasKey $existing.data $key) }}{{- $existingValue = (index $existing.data $key | b64dec) }}{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +type: Opaque +stringData: + {{ $key }}: {{ default (default (randAlphaNum 64) $existingValue) $explicit | quote }} +{{- end }} diff --git a/charts/orka/templates/scm-egress-proxy-service.yaml b/charts/orka/templates/scm-egress-proxy-service.yaml new file mode 100644 index 000000000..8821b4450 --- /dev/null +++ b/charts/orka/templates/scm-egress-proxy-service.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +spec: + selector: + {{- include "orka.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy + ports: + - {name: http-proxy, port: 8080, targetPort: http-proxy} +{{- end }} diff --git a/charts/orka/templates/scm-egress-proxy-serviceaccount.yaml b/charts/orka/templates/scm-egress-proxy-serviceaccount.yaml new file mode 100644 index 000000000..7d1e0fa27 --- /dev/null +++ b/charts/orka/templates/scm-egress-proxy-serviceaccount.yaml @@ -0,0 +1,10 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +automountServiceAccountToken: false +{{- end }} diff --git a/charts/orka/templates/serviceaccount.yaml b/charts/orka/templates/serviceaccount.yaml index e7c65e6b9..a494cdb5c 100644 --- a/charts/orka/templates/serviceaccount.yaml +++ b/charts/orka/templates/serviceaccount.yaml @@ -11,6 +11,21 @@ metadata: {{- end }} {{- end }} --- +{{- if and .Values.publisher.enabled .Values.publisher.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "orka.publisherServiceAccountName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher + {{- with .Values.publisher.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: false +{{- end }} +--- {{- if .Values.client.create }} apiVersion: v1 kind: ServiceAccount @@ -48,11 +63,3 @@ metadata: {{- include "orka.labels" . | nindent 4 }} orka.ai/worker: "true" orka.ai/worker-trust: "container" ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "orka.harnessWrapperName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper diff --git a/charts/orka/templates/store-pvc.yaml b/charts/orka/templates/store-pvc.yaml index b13ba6e81..9463d9224 100644 --- a/charts/orka/templates/store-pvc.yaml +++ b/charts/orka/templates/store-pvc.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: {{ include "orka.fullname" . }}-store + name: {{ include "orka.storeName" . }} labels: {{- include "orka.labels" . | nindent 4 }} app.kubernetes.io/component: store diff --git a/charts/orka/templates/vekil-ingress-networkpolicy.yaml b/charts/orka/templates/vekil-ingress-networkpolicy.yaml new file mode 100644 index 000000000..390089289 --- /dev/null +++ b/charts/orka/templates/vekil-ingress-networkpolicy.yaml @@ -0,0 +1,24 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.vekilIngressPolicyName" . }} + namespace: vekil-system + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: vekil + policyTypes: [Ingress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: + orka.ai/network-role: provider-auth-proxy + ports: [{protocol: TCP, port: 1337}] +{{- end }} diff --git a/charts/orka/values.yaml b/charts/orka/values.yaml index 355c8abcf..1aedb462d 100644 --- a/charts/orka/values.yaml +++ b/charts/orka/values.yaml @@ -9,6 +9,7 @@ controller: image: repository: ghcr.io/orka-agents/orka tag: "0.1.1" + digest: "" pullPolicy: IfNotPresent # Resource limits @@ -44,6 +45,37 @@ controller: # Max active tasks per namespace (0 = unlimited) maxTasksPerNamespace: 0 + # Operation-scoped ACP artifact transport stored on the controller PVC. + acpArtifact: + existingSecret: "" + secretKey: capability-secret + secret: "" + maxBytes: 536870912 + + # Managed ACP core RuntimePools for built-in agent Tasks. + acpRuntime: + # The supported agent execution path is ACP-only; there is no legacy fallback. + enabled: false + namespace: orka-runtimes + # The chart-managed provider proxy always runs in the Helm release namespace. + # Leave empty or set exactly to .Release.Namespace; other values are rejected. + providerProxyNamespace: "" + # Configure each provider that should be available with an immutable + # repository@sha256: reference. Empty providers remain unavailable + # and Tasks selecting them fail closed. + codexImage: "" + claudeImage: "" + copilotImage: "" + # Planned controller upgrades use a same-binary preStop child to call a + # loopback-only coordinator. Keep terminationGracePeriodSeconds greater + # than timeout and triggerTimeout so SIGTERM shutdown still has headroom. + upgradeDrain: + enabled: true + port: 8083 + timeout: 5m + pollInterval: 1s + triggerTimeout: 5m15s + terminationGracePeriodSeconds: 360 # Provider-neutral external gateway plane. gateway: enabled: true @@ -173,6 +205,128 @@ controller: childTokenTTL: "" toolTokenTTL: "" +# Clean-room Workspace/Publisher service. +publisher: + enabled: true + image: + repository: ghcr.io/orka-agents/orka/workspace-publisher + tag: "0.1.1" + digest: "" + pullPolicy: IfNotPresent + allowedSCMHosts: github.com + githubPR: + enabled: true + apiBaseURL: https://api.github.com + requestTimeout: 15s + maxResponseBytes: 4194304 + auth: + existingSecret: "" + controllerTokenKey: controller-token + capabilitySecretKey: operation-capability-secret + controllerToken: "" + capabilitySecret: "" + # Non-secret revision marker. When rotating the publisher auth Secret, bump + # this value in the same Helm upgrade to restart controller and publisher. + rolloutNonce: "" + serviceAccount: + create: true + name: "" + annotations: {} + persistence: + size: 2Gi + storageClass: "" + resources: + requests: + cpu: 100m + memory: 256Mi + ephemeral-storage: 512Mi + limits: + cpu: "2" + memory: 2Gi + ephemeral-storage: 2Gi + +# Authenticated, exact-host HTTPS egress boundary for the Workspace/Publisher. +# The Publisher has no direct public 443 egress; Git and forge API traffic must +# use this proxy, which re-resolves and validates every outbound connection. +scmEgressProxy: + enabled: true + noProxy: localhost,127.0.0.1,::1,.svc,.cluster.local + maxRequestHeaderBytes: 32768 + maxResponseHeaderBytes: 65536 + maxRequestBytes: 4194304 + maxResponseBytes: 8388608 + maxTunnelBytes: 1073741824 + maxConcurrent: 8 + resolutionTimeout: 5s + connectTimeout: 10s + responseHeaderTimeout: 30s + forwardTimeout: 2m + idleTimeout: 30s + tunnelTimeout: 10m + auth: + existingSecret: "" + tokenKey: token + # Existing Secret tokens must use only RFC 3986 unreserved characters and + # be 32-256 bytes so they can be carried in authenticated proxy userinfo. + token: "" + # Non-secret revision marker. When rotating the SCM proxy auth Secret, bump + # this value in the same Helm upgrade to restart publisher and SCM proxy. + rolloutNonce: "" + resources: + requests: + cpu: 25m + memory: 32Mi + ephemeral-storage: 32Mi + limits: + cpu: 500m + memory: 256Mi + ephemeral-storage: 128Mi + +# Authenticated boundary in front of the otherwise unauthenticated Vekil +# service. ACP RuntimePods can reach only this Service, never Vekil directly. +providerProxy: + enabled: false + # Only this chart-supported Vekil Service endpoint is accepted. One trailing + # slash is normalized; alternate hosts, namespaces, and ports are rejected. + upstreamBaseURL: http://vekil.vekil-system.svc:1337 + # Mounted Secret files are reloaded atomically; no proxy Pod restart is + # required for token changes. Reload failures make readiness fail and disable + # all authenticated forwarding until both files are valid again. + tokenReloadInterval: 5s + # Maximum remaining lifetime accepted from the overlap token's absolute + # deadline. Periodic reloads and Pod restarts cannot extend it. Maximum: 24h. + previousTokenOverlap: 10m + # Optional operator-controlled restart input for binary/flag changes or + # recovery. Change it to force only the provider-auth-proxy Pod to roll. + rolloutNonce: "" + auth: + existingSecret: "" + tokenKey: token + token: "" + # Optional overlap key/value. Existing single-token Secrets remain valid + # when this key is absent. For proxy-first rotation, publish new as current + # and old here, wait for proxy reload, then roll controller/runtime pools. + # For controller-first use, first pre-stage new here while current remains + # old and verify it through the proxy; only then publish new as current and + # old here and roll controller/runtime pools. The pre-staged token covers a + # controller request that arrives before the proxy observes the role swap. + # Remove the old token after all workloads advertise the new generation. + # previousTokenValidUntil must be an absolute RFC3339/RFC3339Nano time no + # later than previousTokenOverlap from when the proxy loads it. + previousTokenKey: previous-token + previousToken: "" + previousTokenValidUntilKey: previous-token-valid-until + previousTokenValidUntil: "" + resources: + requests: + cpu: 25m + memory: 32Mi + ephemeral-storage: 32Mi + limits: + cpu: 250m + memory: 128Mi + ephemeral-storage: 128Mi + # Worker configuration workers: ai: @@ -186,21 +340,6 @@ workers: repository: ghcr.io/orka-agents/orka/general-worker tag: "0.1.1" - harnessWrapper: - image: - repository: ghcr.io/orka-agents/orka/agent-harness-wrapper - tag: "0.1.1" - pullPolicy: IfNotPresent - auth: - # Existing Secret containing the shared wrapper bearer token. - # When existingSecret and token are both empty, Helm generates a release-local token. - existingSecret: "" - tokenKey: token - token: "" - # Optional Codex CLI sandbox mode for the Codex adapter. - codexSandboxMode: "" - resources: {} - # GitHub webhook and label-trigger configuration. # Configure a repository webhook to POST to /webhooks/github with the secret below. github: @@ -281,7 +420,7 @@ annotations: {} store: path: /data/orka.db persistence: - enabled: true # durable gateway inbox/outbox and Session history require a PVC + enabled: true # ACP control records, gateway history, and Session state require durable RWO storage size: 1Gi storageClass: "" # use cluster default accessMode: ReadWriteOnce diff --git a/cmd/build/helmify/README.md b/cmd/build/helmify/README.md index 51acfca81..5aa8cd06f 100644 --- a/cmd/build/helmify/README.md +++ b/cmd/build/helmify/README.md @@ -6,11 +6,12 @@ This directory is derived from Gatekeeper's `cmd/build/helmify` flow at `make manifests` performs the same staged generation pattern used by Gatekeeper: 1. `controller-gen` refreshes the canonical CRDs under `config/crd/bases`. -2. Kustomize renders `config/default`. -3. This generator copies the static chart inputs and writes every rendered CRD - under `manifest_staging/charts/orka/crds`. -4. Kustomize also writes the next-release installer to - `manifest_staging/deploy/orka.yaml`. +2. Kustomize renders `config/acp-production` as the next-release raw installer. +3. The Helmify Kustomize input renders `config/default`; this generator copies + the static chart inputs and writes every rendered CRD under + `manifest_staging/charts/orka/crds`. +4. The raw installer is written to `manifest_staging/deploy/orka.yaml` with + fail-closed digest placeholders and without CRDs. Only CRDs are generated from the Kustomize stream in this adaptation. Orka's existing non-CRD Helm templates remain static inputs under `static/templates`; diff --git a/cmd/build/helmify/main_test.go b/cmd/build/helmify/main_test.go index 792b28861..93e766c13 100644 --- a/cmd/build/helmify/main_test.go +++ b/cmd/build/helmify/main_test.go @@ -2,6 +2,7 @@ package main import ( "os" + "os/exec" "path/filepath" "strings" "testing" @@ -81,3 +82,214 @@ func TestObjectSetRejectsDuplicateCRDFilenames(t *testing.T) { t.Fatalf("write() error = %v, want duplicate filename error", err) } } + +func helmTemplateStaticChart(t *testing.T, args ...string) (string, error) { + t.Helper() + helm, err := exec.LookPath("helm") + if err != nil { + t.Skip("helm is required for static chart render tests") + } + + commandArgs := []string{"template", "test", "static", "--namespace", "orka-test"} + commandArgs = append(commandArgs, args...) + output, err := exec.Command(helm, commandArgs...).CombinedOutput() + return string(output), err +} + +func requireHelmRender(t *testing.T, args ...string) string { + t.Helper() + output, err := helmTemplateStaticChart(t, args...) + if err != nil { + t.Fatalf("helm template failed: %v\n%s", err, output) + } + return output +} + +func TestStaticChartUsesServicePortForInClusterControllerURLs(t *testing.T) { + rendered := requireHelmRender(t, + "--set", "service.port=18080", + "--set", "controller.apiPort=8080", + ) + + if got := strings.Count(rendered, "--controller-url="); got != 1 { + t.Fatalf("controller URL argument count = %d, want 1", got) + } + if !strings.Contains(rendered, "--controller-url=http://test-orka.orka-test.svc:18080") { + t.Fatalf("controller URL does not use service.port:\n%s", rendered) + } + for _, variable := range []string{ + "ORKA_PUBLISHER_ARTIFACT_AUTHORIZATION_BROKER_URL", + "ORKA_PUBLISHER_ARTIFACT_API_URL", + "ORKA_PUBLISHER_CREDENTIAL_BROKER_URL", + } { + marker := "name: " + variable + "\n value: http://test-orka:18080" + if !strings.Contains(rendered, marker) { + t.Fatalf("%s does not use service.port", variable) + } + } + + service := requireHelmRender(t, + "--set", "service.port=18080", + "--set", "controller.apiPort=8080", + "--show-only", "templates/service.yaml", + ) + if !strings.Contains(service, "port: 18080") || !strings.Contains(service, "targetPort: api") { + t.Fatalf("controller Service does not preserve service port to named API target:\n%s", service) + } +} + +func TestStaticChartProviderProxyConfigurationIsFixedToSupportedBoundary(t *testing.T) { + digest := "sha256:" + strings.Repeat("0", 64) + rendered := requireHelmRender(t, + "--set", "providerProxy.enabled=true", + "--set", "controller.acpRuntime.enabled=true", + "--set", "store.persistence.enabled=true", + "--set-string", "controller.image.digest="+digest, + "--set-string", "publisher.image.digest="+digest, + "--set-string", "controller.acpRuntime.providerProxyNamespace=orka-test", + "--set-string", "providerProxy.upstreamBaseURL=http://vekil.vekil-system.svc:1337/", + ) + + for _, marker := range []string{ + "--acp-provider-proxy-base-url=http://test-orka-provider-auth-proxy.orka-test.svc:8080", + "--acp-provider-proxy-namespace=orka-test", + "--upstream-base-url=http://vekil.vekil-system.svc:1337", + } { + if !strings.Contains(rendered, marker) { + t.Fatalf("rendered provider proxy configuration is missing %q", marker) + } + } + if strings.Contains(rendered, "--upstream-base-url=http://vekil.vekil-system.svc:1337/") { + t.Fatalf("provider upstream trailing slash was not normalized") + } + + providerPolicy := requireHelmRender(t, + "--set", "providerProxy.enabled=true", + "--show-only", "templates/provider-proxy-networkpolicy.yaml", + ) + for _, marker := range []string{ + "kubernetes.io/metadata.name: vekil-system", + "app.kubernetes.io/name: vekil", + "ports: [{protocol: TCP, port: 1337}]", + } { + if !strings.Contains(providerPolicy, marker) { + t.Fatalf("provider proxy NetworkPolicy lost fixed Vekil boundary %q:\n%s", marker, providerPolicy) + } + } + + vekilPolicy := requireHelmRender(t, + "--set", "providerProxy.enabled=true", + "--show-only", "templates/vekil-ingress-networkpolicy.yaml", + ) + for _, marker := range []string{ + "namespace: vekil-system", + "kubernetes.io/metadata.name: orka-test", + "ports: [{protocol: TCP, port: 1337}]", + } { + if !strings.Contains(vekilPolicy, marker) { + t.Fatalf("Vekil ingress NetworkPolicy lost fixed boundary %q:\n%s", marker, vekilPolicy) + } + } +} + +func TestStaticChartRejectsUnsupportedProviderProxyOverrides(t *testing.T) { + tests := []struct { + name string + args []string + wantError string + }{ + { + name: "different namespace", + args: []string{ + "--set", "providerProxy.enabled=true", + "--set-string", "controller.acpRuntime.providerProxyNamespace=other-system", + }, + wantError: "controller.acpRuntime.providerProxyNamespace must be empty or match the Helm release namespace", + }, + { + name: "different upstream host", + args: []string{ + "--set", "providerProxy.enabled=true", + "--set-string", "providerProxy.upstreamBaseURL=http://other.vekil-system.svc:1337", + }, + wantError: "providerProxy.upstreamBaseURL must be http://vekil.vekil-system.svc:1337", + }, + { + name: "different upstream port", + args: []string{ + "--set", "providerProxy.enabled=true", + "--set-string", "providerProxy.upstreamBaseURL=http://vekil.vekil-system.svc:8080", + }, + wantError: "providerProxy.upstreamBaseURL must be http://vekil.vekil-system.svc:1337", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := helmTemplateStaticChart(t, tt.args...) + if err == nil { + t.Fatalf("helm template unexpectedly accepted unsupported provider proxy override:\n%s", output) + } + if !strings.Contains(output, tt.wantError) { + t.Fatalf("helm template error does not contain %q:\n%s", tt.wantError, output) + } + }) + } +} + +func TestStaticChartUsesRegisteredContextTokenTTSEndpointFlag(t *testing.T) { + rendered := requireHelmRender(t, + "--set-string", "controller.contextToken.tts.url=https://tts.example.test/oauth/token", + "--show-only", "templates/deployment.yaml", + ) + + if !strings.Contains(rendered, "--context-token-tts-endpoint=https://tts.example.test/oauth/token") { + t.Fatalf("controller deployment is missing the registered TTS endpoint flag:\n%s", rendered) + } + if strings.Contains(rendered, "--context-token-tts-url=") { + t.Fatalf("controller deployment rendered the unregistered TTS URL flag:\n%s", rendered) + } +} + +func TestStaticChartAuthRolloutNoncesTargetOnlyCredentialConsumers(t *testing.T) { + args := []string{ + "--set-string", "publisher.auth.rolloutNonce=publisher-v2", + "--set-string", "scmEgressProxy.auth.rolloutNonce=scm-v3", + "--set-string", "publisher.auth.controllerToken=publisher-secret-material", + "--set-string", "scmEgressProxy.auth.token=scm-secret-material-0123456789abcd", + } + + controller := requireHelmRender(t, append(args, "--show-only", "templates/deployment.yaml")...) + publisher := requireHelmRender(t, append(args, "--show-only", "templates/publisher-deployment.yaml")...) + scmProxy := requireHelmRender(t, append(args, "--show-only", "templates/scm-egress-proxy-deployment.yaml")...) + providerProxy := requireHelmRender(t, + "--set", "providerProxy.enabled=true", + "--set-string", "publisher.auth.rolloutNonce=publisher-v2", + "--set-string", "scmEgressProxy.auth.rolloutNonce=scm-v3", + "--show-only", "templates/provider-proxy-deployment.yaml", + ) + + publisherNonce := `orka.ai/publisher-auth-rollout-nonce: "publisher-v2"` + scmNonce := `orka.ai/scm-egress-proxy-auth-rollout-nonce: "scm-v3"` + if !strings.Contains(controller, publisherNonce) || strings.Contains(controller, scmNonce) { + t.Fatalf("controller rollout annotations are incorrect:\n%s", controller) + } + if !strings.Contains(publisher, publisherNonce) || !strings.Contains(publisher, scmNonce) { + t.Fatalf("publisher rollout annotations are incorrect:\n%s", publisher) + } + if strings.Contains(scmProxy, publisherNonce) || !strings.Contains(scmProxy, scmNonce) { + t.Fatalf("SCM proxy rollout annotations are incorrect:\n%s", scmProxy) + } + if strings.Contains(providerProxy, publisherNonce) || strings.Contains(providerProxy, scmNonce) { + t.Fatalf("provider proxy received unrelated auth rollout annotations:\n%s", providerProxy) + } + for name, rendered := range map[string]string{ + "controller": controller, + "publisher": publisher, + "SCM proxy": scmProxy, + } { + if strings.Contains(rendered, "secret-material") { + t.Fatalf("%s Pod template annotation render exposed Secret material", name) + } + } +} diff --git a/cmd/build/helmify/static/README.md b/cmd/build/helmify/static/README.md index ef3c5a383..1b171593a 100644 --- a/cmd/build/helmify/static/README.md +++ b/cmd/build/helmify/static/README.md @@ -2,7 +2,7 @@ This chart is generated from `cmd/build/helmify`; edit the generator inputs and run `make manifests` rather than editing generated chart copies directly. It -packages all thirteen canonical Orka CRDs under `crds/`. +packages all 26 canonical Orka CRDs under `crds/`. ## Fresh install @@ -15,6 +15,19 @@ helm install orka charts/orka \ --wait ``` +The provider proxy is disabled by default. Before enabling `providerProxy.enabled=true` (required when `controller.acpRuntime.enabled=true`), install Vekil in `vekil-system`; the chart then installs the exact cross-namespace ingress policy there. The chart-managed provider proxy itself always runs in the Helm release namespace. Leave `controller.acpRuntime.providerProxyNamespace` empty or set it to that release namespace. The only supported upstream is `http://vekil.vekil-system.svc:1337` (an optional trailing slash is normalized); alternate hosts, namespaces, and ports are rejected because the chart does not create matching NetworkPolicies. + +`service.port` is the controller Service port used by controller and Publisher Service URLs. `controller.apiPort` is only the controller container listener and Service target port. + +### Coordinated authentication Secret rotation + +The Publisher and SCM egress proxy read their authentication material at process startup. Rotate each Secret and its non-secret rollout marker in the same Helm upgrade: + +- When rotating `publisher.auth.existingSecret` (or the chart-managed publisher auth values), bump `publisher.auth.rolloutNonce`. The marker is added only to the controller and Publisher Pod templates so both restart onto the same credential generation. +- When rotating `scmEgressProxy.auth.existingSecret` (or the chart-managed SCM proxy token), bump `scmEgressProxy.auth.rolloutNonce`. The marker is added only to the Publisher and SCM proxy Pod templates. + +The nonce is a revision label, not a credential. Never put Secret content in it. A coordinated upgrade may briefly fail closed while Pods roll, but it avoids an indefinite split generation. + CRDs are cluster-scoped and shared by every Orka release. Use `--skip-crds` only when a designated platform or GitOps workflow already manages compatible Orka CRDs for the cluster. @@ -76,7 +89,7 @@ A matching Orka source checkout provides the same guarded flow as competing CRD apply workflows for the same cluster. If another system owns the CRDs, perform the CRD-first step through that system, -wait for all thirteen CRDs to become `Established`, and then upgrade Orka. +wait for all 26 CRDs to become `Established`, and then upgrade Orka. If a previous release was uninstalled, update its retained CRDs first and install the replacement release with `--skip-crds`. diff --git a/cmd/build/helmify/static/templates/NOTES.txt b/cmd/build/helmify/static/templates/NOTES.txt index 46f8b71ee..06acaeed2 100644 --- a/cmd/build/helmify/static/templates/NOTES.txt +++ b/cmd/build/helmify/static/templates/NOTES.txt @@ -1,10 +1,10 @@ Orka has been installed. -Controller: {{ include "orka.fullname" . }}-controller +Controller: {{ include "orka.controllerName" . }} Namespace: {{ .Release.Namespace }} CRD lifecycle: - - A fresh install creates Orka's thirteen cluster-scoped CRDs unless --skip-crds is used. + - A fresh install creates Orka's 26 cluster-scoped CRDs unless --skip-crds is used. - Helm does not update CRDs during helm upgrade. Apply the CRDs from the exact target chart before upgrading the release. - helm uninstall retains the CRDs and all Orka custom resources. @@ -25,7 +25,7 @@ Upgrade guidance: helm show readme {{- else }} -✅ SQLite store is using persistent storage (PVC: {{ include "orka.fullname" . }}-store). +✅ SQLite store is using persistent storage (PVC: {{ include "orka.storeName" . }}). Data will survive pod restarts. {{- end }} diff --git a/cmd/build/helmify/static/templates/_helpers.tpl b/cmd/build/helmify/static/templates/_helpers.tpl index 7764f35bf..c2b1b5d38 100644 --- a/cmd/build/helmify/static/templates/_helpers.tpl +++ b/cmd/build/helmify/static/templates/_helpers.tpl @@ -78,16 +78,85 @@ suffix so long release names cannot collapse all trust tiers to one name. {{- printf "%s-container-worker" (include "orka.fullname" . | trunc 46 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} {{- end }} +{{- define "orka.controllerName" -}} +{{- printf "%s-controller" (include "orka.fullname" . | trunc 52 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.publisherName" -}} +{{- printf "%s-workspace-publisher" (include "orka.fullname" . | trunc 43 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.publisherAuthSecretName" -}} +{{- printf "%s-workspace-publisher-auth" (include "orka.fullname" . | trunc 38 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.acpArtifactSecretName" -}} +{{- printf "%s-acp-artifact-capability" (include "orka.fullname" . | trunc 39 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.providerProxyName" -}} +{{- printf "%s-provider-auth-proxy" (include "orka.fullname" . | trunc 43 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.scmEgressProxyName" -}} +{{- printf "%s-scm-egress-proxy" (include "orka.fullname" . | trunc 46 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.scmEgressProxyAuthSecretName" -}} +{{- printf "%s-scm-egress-proxy-auth" (include "orka.fullname" . | trunc 41 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.storeName" -}} +{{- printf "%s-store" (include "orka.fullname" . | trunc 57 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "orka.vekilIngressPolicyName" -}} +{{- printf "%s-vekil-ingress" (include "orka.fullname" . | trunc 49 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- end }} + {{/* -Create release-scoped harness-wrapper names while reserving room for suffixes -that must remain valid DNS labels (notably the Service name). +Create the name of the workspace publisher ServiceAccount to use. */}} -{{- define "orka.harnessWrapperName" -}} -{{- printf "%s-agent-harness-wrapper" (include "orka.fullname" . | trunc 41 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- define "orka.publisherServiceAccountName" -}} +{{- if .Values.publisher.serviceAccount.create }} +{{- default (include "orka.publisherName" .) .Values.publisher.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.publisher.serviceAccount.name }} +{{- end }} {{- end }} -{{- define "orka.harnessWrapperAuthSecretName" -}} -{{- printf "%s-harness-wrapper-auth" (include "orka.fullname" . | trunc 42 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{/* +Reject mutable ACP runtime image references when a provider image is configured. +An empty provider image leaves that provider unavailable; Tasks still fail closed +because the ACP runtime remains enabled and has no legacy fallback. +*/}} +{{- define "orka.validateACPRuntimeImage" -}} +{{- $name := .name -}} +{{- $ref := default "" .ref -}} +{{- if and $ref (not (regexMatch "^.+@sha256:[0-9a-f]{64}$" $ref)) -}} +{{- fail (printf "%s must be an immutable image reference ending in @sha256:<64 lowercase hex characters>; got %q" $name $ref) -}} +{{- end -}} +{{- end }} + +{{/* +The chart-managed provider proxy is release-namespaced and its NetworkPolicies +are intentionally pinned to the chart-supported Vekil Service. +*/}} +{{- define "orka.validateProviderProxyConfig" -}} +{{- if .Values.providerProxy.enabled -}} +{{- $configuredNamespace := trim (default "" .Values.controller.acpRuntime.providerProxyNamespace) -}} +{{- if and $configuredNamespace (ne $configuredNamespace .Release.Namespace) -}} +{{- fail (printf "controller.acpRuntime.providerProxyNamespace must be empty or match the Helm release namespace %q when providerProxy.enabled=true" .Release.Namespace) -}} +{{- end -}} +{{- $upstream := trimSuffix "/" (trim (default "" .Values.providerProxy.upstreamBaseURL)) -}} +{{- if ne $upstream "http://vekil.vekil-system.svc:1337" -}} +{{- fail "providerProxy.upstreamBaseURL must be http://vekil.vekil-system.svc:1337 (an optional trailing slash is accepted)" -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{- define "orka.providerProxyUpstreamBaseURL" -}} +{{- trimSuffix "/" (trim (default "" .Values.providerProxy.upstreamBaseURL)) -}} {{- end }} @@ -135,3 +204,12 @@ Create release-scoped static worker ClusterRoleBinding names. {{- define "orka.containerWorkerClusterRoleBindingName" -}} {{- printf "%s-container-worker-rolebinding" (include "orka.fullname" .) | trunc 253 | trimSuffix "-" }} {{- end }} + +{{/* Render repository@digest when an immutable digest is configured. */}} +{{- define "orka.imageRef" -}} +{{- if .digest -}} +{{ printf "%s@%s" .repository .digest }} +{{- else -}} +{{ printf "%s:%s" .repository .tag }} +{{- end -}} +{{- end }} diff --git a/cmd/build/helmify/static/templates/acp-artifact-secret.yaml b/cmd/build/helmify/static/templates/acp-artifact-secret.yaml new file mode 100644 index 000000000..3a6651398 --- /dev/null +++ b/cmd/build/helmify/static/templates/acp-artifact-secret.yaml @@ -0,0 +1,19 @@ +{{- if not .Values.controller.acpArtifact.existingSecret }} +{{- $secretName := include "orka.acpArtifactSecretName" . }} +{{- $secretKey := .Values.controller.acpArtifact.secretKey | default "capability-secret" }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }} +{{- $existingValue := "" }} +{{- if and $existing (hasKey $existing.data $secretKey) }} +{{- $existingValue = (index $existing.data $secretKey | b64dec) }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: acp-artifact-api +type: Opaque +stringData: + {{ $secretKey }}: {{ default (default (randAlphaNum 64) $existingValue) .Values.controller.acpArtifact.secret | quote }} +{{- end }} diff --git a/cmd/build/helmify/static/templates/deployment.yaml b/cmd/build/helmify/static/templates/deployment.yaml index 31ef45c6e..5eca0f36d 100644 --- a/cmd/build/helmify/static/templates/deployment.yaml +++ b/cmd/build/helmify/static/templates/deployment.yaml @@ -1,13 +1,21 @@ -{{- if and .Values.controller.workspaceProvider.fakeProviderEnabled (not .Values.controller.workspaceProvider.apiEnabled) -}} -{{- fail "controller.workspaceProvider.fakeProviderEnabled requires controller.workspaceProvider.apiEnabled" -}} -{{- end -}} -{{- if and .Values.controller.workspaceProvider.apiEnabled (not .Values.controller.workspaceProvider.classUseAdmission.enabled) -}} -{{- fail "controller.workspaceProvider.apiEnabled requires controller.workspaceProvider.classUseAdmission.enabled" -}} -{{- end -}} -{{- if .Values.controller.workspaceProvider.classUseAdmission.enabled -}} -{{- $workspaceWebhookSecret := required "controller.workspaceProvider.classUseAdmission.existingSecret is required when class-use admission is enabled" .Values.controller.workspaceProvider.classUseAdmission.existingSecret -}} -{{- $workspaceWebhookCA := required "controller.workspaceProvider.classUseAdmission.caBundle is required when class-use admission is enabled" .Values.controller.workspaceProvider.classUseAdmission.caBundle -}} -{{- end -}} +{{- include "orka.validateProviderProxyConfig" . }} +{{- if .Values.controller.acpRuntime.enabled }} +{{- if not (regexMatch "^sha256:[0-9a-f]{64}$" (.Values.controller.image.digest | default "")) }} +{{- fail "controller.image.digest must be a sha256 digest when controller.acpRuntime.enabled=true" }} +{{- end }} +{{- if or (not .Values.publisher.enabled) (not (regexMatch "^sha256:[0-9a-f]{64}$" (.Values.publisher.image.digest | default ""))) }} +{{- fail "publisher must be enabled with publisher.image.digest set when controller.acpRuntime.enabled=true" }} +{{- end }} +{{- if not .Values.store.persistence.enabled }} +{{- fail "store.persistence.enabled must be true when controller.acpRuntime.enabled=true" }} +{{- end }} +{{- include "orka.validateACPRuntimeImage" (dict "name" "controller.acpRuntime.codexImage" "ref" .Values.controller.acpRuntime.codexImage) }} +{{- include "orka.validateACPRuntimeImage" (dict "name" "controller.acpRuntime.claudeImage" "ref" .Values.controller.acpRuntime.claudeImage) }} +{{- include "orka.validateACPRuntimeImage" (dict "name" "controller.acpRuntime.copilotImage" "ref" .Values.controller.acpRuntime.copilotImage) }} +{{- if and .Values.controller.acpRuntime.enabled (not .Values.providerProxy.enabled) }} +{{- fail "providerProxy.enabled must be true when controller.acpRuntime.enabled is true" }} +{{- end }} +{{- end }} {{- $gatewayCRDsReady := false -}} {{- if and .Values.controller.gateway.enabled (not .Values.controller.gateway.crdsReadyOverride) -}} {{- $gatewayClassCRD := lookup "apiextensions.k8s.io/v1" "CustomResourceDefinition" "" "gatewayclasses.gateway.orka.ai" -}} @@ -27,7 +35,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} labels: {{- include "orka.labels" . | nindent 4 }} app.kubernetes.io/component: controller @@ -46,9 +54,15 @@ spec: labels: {{- include "orka.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: controller - {{- with .Values.annotations }} + orka.ai/network-role: controller + {{- if or .Values.annotations .Values.publisher.auth.rolloutNonce }} annotations: + {{- with .Values.annotations }} {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.publisher.auth.rolloutNonce }} + orka.ai/publisher-auth-rollout-nonce: {{ . | quote }} + {{- end }} {{- end }} spec: serviceAccountName: {{ include "orka.serviceAccountName" . }} @@ -56,9 +70,12 @@ spec: securityContext: {{- toYaml . | nindent 8 }} {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.controller.acpRuntime.upgradeDrain.enabled }} + terminationGracePeriodSeconds: {{ .Values.controller.acpRuntime.upgradeDrain.terminationGracePeriodSeconds }} + {{- end }} containers: - name: controller - image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag }}" + image: {{ include "orka.imageRef" .Values.controller.image | quote }} imagePullPolicy: {{ .Values.controller.image.pullPolicy }} args: - --api-port={{ .Values.controller.apiPort }} @@ -70,6 +87,30 @@ spec: - --watch-namespace={{ .Values.controller.watchNamespace }} {{- end }} - --leader-elect={{ .Values.controller.leaderElect }} + - --acp-runtime-enabled={{ .Values.controller.acpRuntime.enabled }} + - --acp-runtime-namespace={{ .Values.controller.acpRuntime.namespace }} + {{- if and .Values.controller.acpRuntime.enabled .Values.controller.acpRuntime.upgradeDrain.enabled }} + - --acp-upgrade-drain-bind-address=127.0.0.1:{{ .Values.controller.acpRuntime.upgradeDrain.port }} + - --acp-upgrade-drain-timeout={{ .Values.controller.acpRuntime.upgradeDrain.timeout }} + - --acp-upgrade-drain-poll-interval={{ .Values.controller.acpRuntime.upgradeDrain.pollInterval }} + - --acp-upgrade-drain-trigger-timeout={{ .Values.controller.acpRuntime.upgradeDrain.triggerTimeout }} + - --acp-upgrade-drain-marker-namespace={{ .Release.Namespace }} + {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.providerProxy.enabled }} + - --acp-provider-proxy-base-url=http://{{ include "orka.providerProxyName" . }}.{{ .Release.Namespace }}.svc:8080 + - --acp-provider-proxy-namespace={{ .Release.Namespace }} + - --acp-provider-proxy-pod-labels=orka.ai/network-role=provider-auth-proxy + - --acp-provider-proxy-token-file=/var/run/orka/provider-auth/token + {{- end }} + {{- if .Values.controller.acpRuntime.codexImage }} + - {{ printf "--acp-codex-runtime-image=%s" .Values.controller.acpRuntime.codexImage | quote }} + {{- end }} + {{- if .Values.controller.acpRuntime.claudeImage }} + - {{ printf "--acp-claude-runtime-image=%s" .Values.controller.acpRuntime.claudeImage | quote }} + {{- end }} + {{- if .Values.controller.acpRuntime.copilotImage }} + - {{ printf "--acp-copilot-runtime-image=%s" .Values.controller.acpRuntime.copilotImage | quote }} + {{- end }} - --zap-log-level={{ .Values.controller.logLevel }} - --gateway-enabled={{ $gatewayRuntimeEnabled }} - --gateway-pending-per-session={{ .Values.controller.gateway.pendingPerSession }} @@ -146,9 +187,6 @@ spec: {{- if .secretRead }} - {{ printf "--context-token-secret-read-scopes=%s" .secretRead | quote }} {{- end }} - {{- if .secretCredentialRead }} - - {{ printf "--context-token-secret-credential-read-scopes=%s" .secretCredentialRead | quote }} - {{- end }} {{- if .agentRead }} - {{ printf "--context-token-agent-read-scopes=%s" .agentRead | quote }} {{- end }} @@ -196,8 +234,8 @@ spec: {{- end }} {{- end }} {{- with .tts }} - {{- if .endpoint }} - - {{ printf "--context-token-tts-endpoint=%s" .endpoint | quote }} + {{- if .url }} + - {{ printf "--context-token-tts-endpoint=%s" .url | quote }} {{- end }} {{- if .audience }} - {{ printf "--context-token-tts-audience=%s" .audience | quote }} @@ -225,14 +263,6 @@ spec: {{- end }} {{- end }} {{- end }} - {{- with .Values.controller.outboundAccess }} - {{- if .trustedGatewayServices }} - - {{ printf "--outbound-access-trusted-gateway-services=%s" (join "," .trustedGatewayServices) | quote }} - {{- end }} - {{- if .trustedTokenEndpointServices }} - - {{ printf "--outbound-access-trusted-token-endpoint-services=%s" (join "," .trustedTokenEndpointServices) | quote }} - {{- end }} - {{- end }} - --store-backend=sqlite - --store-path={{ .Values.store.path }} - --ai-worker-image={{ .Values.workers.ai.image.repository }}:{{ .Values.workers.ai.image.tag }} @@ -250,18 +280,6 @@ spec: {{- if gt (int .Values.controller.maxTasksPerNamespace) 0 }} - --max-tasks-per-namespace={{ .Values.controller.maxTasksPerNamespace }} {{- end }} - {{- with .Values.controller.workspaceProvider }} - {{- if .apiEnabled }} - - --enable-workspace-provider-api=true - {{- end }} - {{- if .fakeProviderEnabled }} - - --enable-fake-workspace-provider=true - {{- end }} - {{- if .classUseAdmission.enabled }} - - --workspace-class-use-admission-enabled=true - - --webhook-cert-path=/var/run/orka/workspace-webhook - {{- end }} - {{- end }} {{- with .Values.controller.executionWorkspace }} - --execution-workspace-default-provider={{ .defaultProvider | default "agent-sandbox" }} {{- end }} @@ -315,12 +333,20 @@ spec: value: {{ include "orka.vendorWorkerClusterRoleName" . | quote }} - name: ORKA_CONTAINER_WORKER_CLUSTER_ROLE_NAME value: {{ include "orka.containerWorkerClusterRoleName" . | quote }} - - name: ORKA_HARNESS_WRAPPER_ENDPOINT - value: http://{{ include "orka.harnessWrapperName" . }}:8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_HARNESS_WRAPPER_SERVICE_ACCOUNT_NAME - value: {{ include "orka.harnessWrapperName" . | quote }} + - name: ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE + value: /var/run/orka/acp-artifacts/capability-secret + - name: ORKA_ACP_ARTIFACT_ROOT + value: /data/acp-artifacts + - name: ORKA_ACP_ARTIFACT_MAX_BYTES + value: {{ .Values.controller.acpArtifact.maxBytes | quote }} + {{- if .Values.publisher.enabled }} + - name: ORKA_WORKSPACE_PUBLISHER_URL + value: http://{{ include "orka.publisherName" . }}:8080 + - name: ORKA_WORKSPACE_PUBLISHER_CONTROLLER_TOKEN_FILE + value: /var/run/orka/publisher-auth/controller-token + - name: ORKA_WORKSPACE_PUBLISHER_CAPABILITY_SECRET_FILE + value: /var/run/orka/publisher-auth/operation-capability-secret + {{- end }} {{- if .Values.github.webhook.secretName }} - name: ORKA_GITHUB_WEBHOOK_SECRET valueFrom: @@ -368,11 +394,15 @@ spec: - name: health containerPort: {{ .Values.controller.healthPort }} protocol: TCP - {{- if .Values.controller.workspaceProvider.classUseAdmission.enabled }} - - name: webhook-server - containerPort: 9443 - protocol: TCP - {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.controller.acpRuntime.upgradeDrain.enabled }} + lifecycle: + preStop: + exec: + command: + - /manager + - {{ printf "--acp-upgrade-drain-trigger-url=http://127.0.0.1:%v%s" .Values.controller.acpRuntime.upgradeDrain.port "/acp/upgrade-drain" | quote }} + - {{ printf "--acp-upgrade-drain-trigger-timeout=%s" .Values.controller.acpRuntime.upgradeDrain.triggerTimeout | quote }} + {{- end }} livenessProbe: httpGet: path: /healthz @@ -398,41 +428,56 @@ spec: mountPath: /tmp - name: store mountPath: /data - - name: harness-wrapper-auth - mountPath: /var/run/orka/harness-wrapper + - name: acp-artifact-capability + mountPath: /var/run/orka/acp-artifacts readOnly: true - {{- if .Values.controller.workspaceProvider.classUseAdmission.enabled }} - - name: workspace-webhook-certs - mountPath: /var/run/orka/workspace-webhook + {{- if .Values.publisher.enabled }} + - name: workspace-publisher-auth + mountPath: /var/run/orka/publisher-auth + readOnly: true + {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.providerProxy.enabled }} + - name: provider-auth-proxy + mountPath: /var/run/orka/provider-auth readOnly: true {{- end }} volumes: - name: tmp emptyDir: {} - - name: harness-wrapper-auth + - name: acp-artifact-capability secret: - secretName: {{ .Values.workers.harnessWrapper.auth.existingSecret | default (include "orka.harnessWrapperAuthSecretName" .) }} - items: - - key: {{ .Values.workers.harnessWrapper.auth.tokenKey | default "token" }} - path: token - {{- if .Values.controller.workspaceProvider.classUseAdmission.enabled }} - - name: workspace-webhook-certs - secret: - secretName: {{ .Values.controller.workspaceProvider.classUseAdmission.existingSecret | quote }} + secretName: {{ .Values.controller.acpArtifact.existingSecret | default (include "orka.acpArtifactSecretName" .) }} defaultMode: 0400 items: - - key: tls.crt - path: tls.crt - - key: tls.key - path: tls.key - {{- end }} + - key: {{ .Values.controller.acpArtifact.secretKey | default "capability-secret" }} + path: capability-secret - name: store {{- if .Values.store.persistence.enabled }} persistentVolumeClaim: - claimName: {{ include "orka.fullname" . }}-store + claimName: {{ include "orka.storeName" . }} {{- else }} emptyDir: {} {{- end }} + {{- if .Values.publisher.enabled }} + - name: workspace-publisher-auth + secret: + secretName: {{ .Values.publisher.auth.existingSecret | default (include "orka.publisherAuthSecretName" .) }} + defaultMode: 0400 + items: + - key: {{ .Values.publisher.auth.controllerTokenKey | default "controller-token" }} + path: controller-token + - key: {{ .Values.publisher.auth.capabilitySecretKey | default "operation-capability-secret" }} + path: operation-capability-secret + {{- end }} + {{- if and .Values.controller.acpRuntime.enabled .Values.providerProxy.enabled }} + - name: provider-auth-proxy + secret: + secretName: {{ .Values.providerProxy.auth.existingSecret | default (include "orka.providerProxyName" .) }} + defaultMode: 0400 + items: + - key: {{ .Values.providerProxy.auth.tokenKey | default "token" }} + path: token + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/cmd/build/helmify/static/templates/harness-wrapper-deployment.yaml b/cmd/build/helmify/static/templates/harness-wrapper-deployment.yaml deleted file mode 100644 index 7d42a6b64..000000000 --- a/cmd/build/helmify/static/templates/harness-wrapper-deployment.yaml +++ /dev/null @@ -1,110 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "orka.harnessWrapperName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper - template: - metadata: - labels: - {{- include "orka.selectorLabels" . | nindent 8 }} - app.kubernetes.io/component: agent-harness-wrapper - spec: - serviceAccountName: {{ include "orka.harnessWrapperName" . }} - automountServiceAccountToken: false - securityContext: - runAsUser: 0 - runAsGroup: 0 - seccompProfile: - type: RuntimeDefault - containers: - - name: wrapper - image: {{ .Values.workers.harnessWrapper.image.repository }}:{{ .Values.workers.harnessWrapper.image.tag }} - imagePullPolicy: {{ .Values.workers.harnessWrapper.image.pullPolicy }} - ports: - - name: http - containerPort: 8080 - protocol: TCP - env: - - name: ORKA_HARNESS_WRAPPER_RUNTIME - value: multi - - name: ORKA_HARNESS_WRAPPER_LISTEN_ADDR - value: :8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_ALLOW_BASH - value: "true" - - name: ORKA_HARNESS_WRAPPER_CHILD_UID - value: "1000" - - name: ORKA_HARNESS_WRAPPER_CHILD_GID - value: "1000" - {{- if .Values.workers.harnessWrapper.codexSandboxMode }} - - name: ORKA_CODEX_SANDBOX_MODE - value: {{ .Values.workers.harnessWrapper.codexSandboxMode | quote }} - {{- end }} - - name: ORKA_SA_TOKEN_PATH - value: /var/run/orka/upload-token/token - volumeMounts: - - name: auth - mountPath: /var/run/orka/harness-wrapper - readOnly: true - - name: upload-token - mountPath: /var/run/orka/upload-token - readOnly: true - - name: tmp - mountPath: /tmp - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsUser: 0 - runAsGroup: 0 - capabilities: - drop: - - ALL - add: - - SETUID - - SETGID - - CHOWN - - KILL - - FOWNER - livenessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 10 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.workers.harnessWrapper.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: upload-token - projected: - defaultMode: 0400 - sources: - - serviceAccountToken: - path: token - - name: auth - secret: - secretName: {{ .Values.workers.harnessWrapper.auth.existingSecret | default (include "orka.harnessWrapperAuthSecretName" .) }} - defaultMode: 0400 - items: - - key: {{ .Values.workers.harnessWrapper.auth.tokenKey | default "token" }} - path: token - - name: tmp - emptyDir: {} diff --git a/cmd/build/helmify/static/templates/harness-wrapper-secret.yaml b/cmd/build/helmify/static/templates/harness-wrapper-secret.yaml deleted file mode 100644 index 4a3a5c968..000000000 --- a/cmd/build/helmify/static/templates/harness-wrapper-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if not .Values.workers.harnessWrapper.auth.existingSecret }} -{{- $secretName := include "orka.harnessWrapperAuthSecretName" . }} -{{- $tokenKey := .Values.workers.harnessWrapper.auth.tokenKey | default "token" }} -{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }} -{{- $existingToken := "" }} -{{- if and $existing (hasKey $existing.data $tokenKey) }} -{{- $existingToken = (index $existing.data $tokenKey | b64dec) }} -{{- end }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ $secretName }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper -type: Opaque -stringData: - {{ $tokenKey }}: {{ default (default (randAlphaNum 32) $existingToken) .Values.workers.harnessWrapper.auth.token | quote }} -{{- end }} diff --git a/cmd/build/helmify/static/templates/provider-proxy-deployment.yaml b/cmd/build/helmify/static/templates/provider-proxy-deployment.yaml new file mode 100644 index 000000000..c3ab9e123 --- /dev/null +++ b/cmd/build/helmify/static/templates/provider-proxy-deployment.yaml @@ -0,0 +1,82 @@ +{{- include "orka.validateProviderProxyConfig" . }} +{{- if .Values.providerProxy.enabled }} +{{- $secretName := .Values.providerProxy.auth.existingSecret | default (include "orka.providerProxyName" .) }} +{{- $currentKey := .Values.providerProxy.auth.tokenKey | default "token" }} +{{- $previousKey := .Values.providerProxy.auth.previousTokenKey | default "previous-token" }} +{{- $previousValidUntilKey := .Values.providerProxy.auth.previousTokenValidUntilKey | default "previous-token-valid-until" }} +{{- if or (eq $currentKey $previousKey) (eq $currentKey $previousValidUntilKey) (eq $previousKey $previousValidUntilKey) }} +{{- fail "providerProxy auth token keys must differ" }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +spec: + replicas: 1 + strategy: {type: Recreate} + selector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: provider-auth-proxy + template: + metadata: + {{- with .Values.providerProxy.rolloutNonce }} + annotations: + orka.ai/provider-auth-rollout-nonce: {{ . | quote }} + {{- end }} + labels: + {{- include "orka.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: provider-auth-proxy + orka.ai/network-role: provider-auth-proxy + spec: + serviceAccountName: {{ include "orka.providerProxyName" . }} + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: {type: RuntimeDefault} + containers: + - name: proxy + image: {{ include "orka.imageRef" .Values.controller.image | quote }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + command: [/provider-auth-proxy] + args: + - --listen-address=:8080 + - {{ printf "--upstream-base-url=%s" (include "orka.providerProxyUpstreamBaseURL" .) | quote }} + - --token-file=/var/run/secrets/orka/provider-auth/token + - --previous-token-file=/var/run/secrets/orka/provider-auth/previous-token + - --previous-token-valid-until-file=/var/run/secrets/orka/provider-auth/previous-token-valid-until + - {{ printf "--token-reload-interval=%s" (.Values.providerProxy.tokenReloadInterval | default "5s") | quote }} + - {{ printf "--previous-token-overlap=%s" (.Values.providerProxy.previousTokenOverlap | default "10m") | quote }} + ports: [{name: http, containerPort: 8080}] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: + {{- toYaml .Values.providerProxy.resources | nindent 12 }} + readinessProbe: {httpGet: {path: /readyz, port: http}} + livenessProbe: {httpGet: {path: /healthz, port: http}} + volumeMounts: + - {name: provider-auth, mountPath: /var/run/secrets/orka/provider-auth, readOnly: true} + volumes: + - name: provider-auth + projected: + defaultMode: 0440 + sources: + - secret: + name: {{ $secretName }} + optional: true + items: + - key: {{ $currentKey }} + path: token + - key: {{ $previousKey }} + path: previous-token + - key: {{ $previousValidUntilKey }} + path: previous-token-valid-until +{{- end }} diff --git a/cmd/build/helmify/static/templates/provider-proxy-networkpolicy.yaml b/cmd/build/helmify/static/templates/provider-proxy-networkpolicy.yaml new file mode 100644 index 000000000..fabca2ebe --- /dev/null +++ b/cmd/build/helmify/static/templates/provider-proxy-networkpolicy.yaml @@ -0,0 +1,36 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: provider-auth-proxy + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.controller.acpRuntime.namespace }} + podSelector: + matchLabels: + orka.ai/network-role: provider-client + ports: [{protocol: TCP, port: 8080}] + egress: + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kube-system} + podSelector: + matchLabels: {k8s-app: kube-dns} + ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}] + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: vekil-system} + podSelector: + matchLabels: {app.kubernetes.io/name: vekil} + ports: [{protocol: TCP, port: 1337}] +{{- end }} diff --git a/cmd/build/helmify/static/templates/provider-proxy-secret.yaml b/cmd/build/helmify/static/templates/provider-proxy-secret.yaml new file mode 100644 index 000000000..068115022 --- /dev/null +++ b/cmd/build/helmify/static/templates/provider-proxy-secret.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.providerProxy.enabled (not .Values.providerProxy.auth.existingSecret) }} +{{- $name := include "orka.providerProxyName" . }} +{{- $key := .Values.providerProxy.auth.tokenKey | default "token" }} +{{- $previousKey := .Values.providerProxy.auth.previousTokenKey | default "previous-token" }} +{{- $previousValidUntilKey := .Values.providerProxy.auth.previousTokenValidUntilKey | default "previous-token-valid-until" }} +{{- $previousToken := .Values.providerProxy.auth.previousToken | default "" }} +{{- $previousValidUntil := .Values.providerProxy.auth.previousTokenValidUntil | default "" }} +{{- if or (eq $key $previousKey) (eq $key $previousValidUntilKey) (eq $previousKey $previousValidUntilKey) }} +{{- fail "providerProxy auth token keys must differ" }} +{{- end }} +{{- if ne (empty $previousToken) (empty $previousValidUntil) }} +{{- fail "providerProxy.auth.previousToken and previousTokenValidUntil must be set together" }} +{{- end }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name }} +{{- $existingValue := "" }} +{{- if and $existing (hasKey $existing.data $key) }}{{- $existingValue = (index $existing.data $key | b64dec) }}{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +type: Opaque +data: + {{ $key }}: {{ default (default (randAlphaNum 64) $existingValue) .Values.providerProxy.auth.token | toString | b64enc | quote }} + {{- if $previousToken }} + {{ $previousKey }}: {{ $previousToken | toString | b64enc | quote }} + {{ $previousValidUntilKey }}: {{ $previousValidUntil | toString | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/cmd/build/helmify/static/templates/provider-proxy-service.yaml b/cmd/build/helmify/static/templates/provider-proxy-service.yaml new file mode 100644 index 000000000..f79e37bbb --- /dev/null +++ b/cmd/build/helmify/static/templates/provider-proxy-service.yaml @@ -0,0 +1,15 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +spec: + selector: + {{- include "orka.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy + ports: + - {name: http, port: 8080, targetPort: http} +{{- end }} diff --git a/cmd/build/helmify/static/templates/provider-proxy-serviceaccount.yaml b/cmd/build/helmify/static/templates/provider-proxy-serviceaccount.yaml new file mode 100644 index 000000000..b066ee20e --- /dev/null +++ b/cmd/build/helmify/static/templates/provider-proxy-serviceaccount.yaml @@ -0,0 +1,10 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "orka.providerProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +automountServiceAccountToken: false +{{- end }} diff --git a/cmd/build/helmify/static/templates/publisher-deployment.yaml b/cmd/build/helmify/static/templates/publisher-deployment.yaml new file mode 100644 index 000000000..07d7d2005 --- /dev/null +++ b/cmd/build/helmify/static/templates/publisher-deployment.yaml @@ -0,0 +1,113 @@ +{{- if and .Values.publisher.enabled (not .Values.scmEgressProxy.enabled) }} +{{- fail "publisher.enabled requires scmEgressProxy.enabled so public HTTPS egress remains proxy-only" }} +{{- end }} +{{- if .Values.publisher.enabled }} +{{- $scmProxySecretName := .Values.scmEgressProxy.auth.existingSecret | default (include "orka.scmEgressProxyAuthSecretName" .) }} +{{- $scmProxyTokenKey := .Values.scmEgressProxy.auth.tokenKey | default "token" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "orka.publisherName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher +spec: + replicas: 1 + strategy: {type: Recreate} + selector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: workspace-publisher + template: + metadata: + {{- if or .Values.publisher.auth.rolloutNonce .Values.scmEgressProxy.auth.rolloutNonce }} + annotations: + {{- with .Values.publisher.auth.rolloutNonce }} + orka.ai/publisher-auth-rollout-nonce: {{ . | quote }} + {{- end }} + {{- with .Values.scmEgressProxy.auth.rolloutNonce }} + orka.ai/scm-egress-proxy-auth-rollout-nonce: {{ . | quote }} + {{- end }} + {{- end }} + labels: + {{- include "orka.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: workspace-publisher + orka.ai/network-role: workspace-publisher + spec: + serviceAccountName: {{ include "orka.publisherServiceAccountName" . }} + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: {type: RuntimeDefault} + containers: + - name: publisher + image: {{ include "orka.imageRef" .Values.publisher.image | quote }} + imagePullPolicy: {{ .Values.publisher.image.pullPolicy }} + ports: + - {name: http, containerPort: 8080} + env: + - name: ORKA_SCM_EGRESS_PROXY_TOKEN + valueFrom: + secretKeyRef: + name: {{ $scmProxySecretName }} + key: {{ $scmProxyTokenKey }} + - name: HTTPS_PROXY + value: {{ printf "http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@%s.%s.svc:%v" (include "orka.scmEgressProxyName" .) .Release.Namespace 8080 | quote }} + - name: https_proxy + value: {{ printf "http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@%s.%s.svc:%v" (include "orka.scmEgressProxyName" .) .Release.Namespace 8080 | quote }} + - {name: NO_PROXY, value: {{ .Values.scmEgressProxy.noProxy | quote }}} + - {name: no_proxy, value: {{ .Values.scmEgressProxy.noProxy | quote }}} + - {name: ORKA_PUBLISHER_SCM_EGRESS_PROXY_REQUIRED, value: "true"} + - {name: ORKA_PUBLISHER_LISTEN_ADDRESS, value: ":8080"} + - {name: ORKA_PUBLISHER_TEMP_ROOT, value: /tmp/orka-workspace-publisher/runtime} + - {name: ORKA_PUBLISHER_CONTROLLER_TOKEN_FILE, value: /var/run/orka/publisher-auth/controller-token} + - {name: ORKA_PUBLISHER_OPERATION_CAPABILITY_SECRET_FILE, value: /var/run/orka/publisher-auth/operation-capability-secret} + - name: ORKA_PUBLISHER_ARTIFACT_AUTHORIZATION_BROKER_URL + value: http://{{ include "orka.fullname" . }}:{{ .Values.service.port }} + - name: ORKA_PUBLISHER_ARTIFACT_API_URL + value: http://{{ include "orka.fullname" . }}:{{ .Values.service.port }} + - name: ORKA_PUBLISHER_CREDENTIAL_BROKER_URL + value: http://{{ include "orka.fullname" . }}:{{ .Values.service.port }} + - {name: ORKA_PUBLISHER_ALLOWED_SCM_HOSTS, value: {{ .Values.publisher.allowedSCMHosts | quote }}} + - {name: ORKA_PUBLISHER_GITHUB_PR_ENABLED, value: {{ .Values.publisher.githubPR.enabled | quote }}} + {{- if .Values.publisher.githubPR.enabled }} + - {name: ORKA_PUBLISHER_GITHUB_API_BASE_URL, value: {{ .Values.publisher.githubPR.apiBaseURL | quote }}} + - {name: ORKA_PUBLISHER_GITHUB_REQUEST_TIMEOUT, value: {{ .Values.publisher.githubPR.requestTimeout | quote }}} + - {name: ORKA_PUBLISHER_GITHUB_MAX_RESPONSE_BYTES, value: {{ .Values.publisher.githubPR.maxResponseBytes | quote }}} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: + {{- toYaml .Values.publisher.resources | nindent 12 }} + volumeMounts: + - {name: data, mountPath: /data} + - {name: tmp, mountPath: /tmp/orka-workspace-publisher} + - {name: publisher-auth, mountPath: /var/run/orka/publisher-auth/controller-token, subPath: controller-token, readOnly: true} + - {name: publisher-auth, mountPath: /var/run/orka/publisher-auth/operation-capability-secret, subPath: operation-capability-secret, readOnly: true} + readinessProbe: + httpGet: {path: /v1/health, port: http} + livenessProbe: + httpGet: {path: /v1/health, port: http} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "orka.publisherName" . }} + - name: tmp + emptyDir: {sizeLimit: 1Gi} + - name: publisher-auth + secret: + secretName: {{ .Values.publisher.auth.existingSecret | default (include "orka.publisherAuthSecretName" .) }} + # subPath bind mounts expose regular files to the fail-closed + # publisher loader; fsGroup grants only the Pod group read access. + defaultMode: 0440 + items: + - key: {{ .Values.publisher.auth.controllerTokenKey | default "controller-token" }} + path: controller-token + - key: {{ .Values.publisher.auth.capabilitySecretKey | default "operation-capability-secret" }} + path: operation-capability-secret +{{- end }} diff --git a/cmd/build/helmify/static/templates/publisher-networkpolicy.yaml b/cmd/build/helmify/static/templates/publisher-networkpolicy.yaml new file mode 100644 index 000000000..281274ea8 --- /dev/null +++ b/cmd/build/helmify/static/templates/publisher-networkpolicy.yaml @@ -0,0 +1,40 @@ +{{- if .Values.publisher.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.publisherName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: workspace-publisher + policyTypes: [Ingress, Egress] + ingress: + - from: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: controller + ports: [{protocol: TCP, port: 8080}] + egress: + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kube-system} + podSelector: + matchLabels: {k8s-app: kube-dns} + ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}] + - to: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: controller + ports: [{protocol: TCP, port: {{ .Values.controller.apiPort }}}] + - to: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: scm-egress-proxy + ports: [{protocol: TCP, port: 8080}] +{{- end }} diff --git a/cmd/build/helmify/static/templates/publisher-pvc.yaml b/cmd/build/helmify/static/templates/publisher-pvc.yaml new file mode 100644 index 000000000..25417e82b --- /dev/null +++ b/cmd/build/helmify/static/templates/publisher-pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.publisher.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "orka.publisherName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher +spec: + accessModes: [ReadWriteOnce] + {{- if .Values.publisher.persistence.storageClass }} + storageClassName: {{ .Values.publisher.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.publisher.persistence.size }} +{{- end }} diff --git a/cmd/build/helmify/static/templates/publisher-secret.yaml b/cmd/build/helmify/static/templates/publisher-secret.yaml new file mode 100644 index 000000000..64e1403de --- /dev/null +++ b/cmd/build/helmify/static/templates/publisher-secret.yaml @@ -0,0 +1,23 @@ +{{- if and .Values.publisher.enabled (not .Values.publisher.auth.existingSecret) }} +{{- $name := include "orka.publisherAuthSecretName" . }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name }} +{{- $controllerKey := .Values.publisher.auth.controllerTokenKey | default "controller-token" }} +{{- $capabilityKey := .Values.publisher.auth.capabilitySecretKey | default "operation-capability-secret" }} +{{- $existingController := "" }} +{{- $existingCapability := "" }} +{{- if $existing }} +{{- if hasKey $existing.data $controllerKey }}{{- $existingController = (index $existing.data $controllerKey | b64dec) }}{{- end }} +{{- if hasKey $existing.data $capabilityKey }}{{- $existingCapability = (index $existing.data $capabilityKey | b64dec) }}{{- end }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher +type: Opaque +stringData: + {{ $controllerKey }}: {{ default (default (randAlphaNum 64) $existingController) .Values.publisher.auth.controllerToken | quote }} + {{ $capabilityKey }}: {{ default (default (randAlphaNum 64) $existingCapability) .Values.publisher.auth.capabilitySecret | quote }} +{{- end }} diff --git a/manifest_staging/charts/orka/templates/harness-wrapper-service.yaml b/cmd/build/helmify/static/templates/publisher-service.yaml similarity index 54% rename from manifest_staging/charts/orka/templates/harness-wrapper-service.yaml rename to cmd/build/helmify/static/templates/publisher-service.yaml index e6d5ddf10..9b18963a1 100644 --- a/manifest_staging/charts/orka/templates/harness-wrapper-service.yaml +++ b/cmd/build/helmify/static/templates/publisher-service.yaml @@ -1,16 +1,17 @@ +{{- if .Values.publisher.enabled }} apiVersion: v1 kind: Service metadata: - name: {{ include "orka.harnessWrapperName" . }} + name: {{ include "orka.publisherName" . }} labels: {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper + app.kubernetes.io/component: workspace-publisher spec: selector: {{- include "orka.selectorLabels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper + app.kubernetes.io/component: workspace-publisher ports: - name: http port: 8080 targetPort: http - protocol: TCP +{{- end }} diff --git a/cmd/build/helmify/static/templates/rbac.yaml b/cmd/build/helmify/static/templates/rbac.yaml index be674d840..b218acf5a 100644 --- a/cmd/build/helmify/static/templates/rbac.yaml +++ b/cmd/build/helmify/static/templates/rbac.yaml @@ -3,19 +3,19 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} labels: {{- include "orka.labels" . | nindent 4 }} rules: # Custom Resource permissions - apiGroups: ["core.orka.ai"] - resources: ["tasks", "tools", "agents", "agentruntimes", "providers", "skills", "repositorymonitors", "repositoryscans", "substrateactorpools", "outboundaccesspolicies"] + resources: ["tasks", "tools", "agents", "agentruntimes", "runtimepools", "promptattempts", "runtimesessioncontrols", "branchclaims", "publications", "controllerepochs", "externaleffects", "providers", "skills", "repositorymonitors", "repositoryscans", "substrateactorpools"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["core.orka.ai"] - resources: ["tasks/status", "tools/status", "agents/status", "agentruntimes/status", "providers/status", "skills/status", "repositorymonitors/status", "repositoryscans/status", "substrateactorpools/status", "outboundaccesspolicies/status"] + resources: ["tasks/status", "tools/status", "agents/status", "agentruntimes/status", "runtimepools/status", "promptattempts/status", "runtimesessioncontrols/status", "branchclaims/status", "publications/status", "controllerepochs/status", "externaleffects/status", "providers/status", "skills/status", "repositorymonitors/status", "repositoryscans/status", "substrateactorpools/status"] verbs: ["get", "update", "patch"] - apiGroups: ["core.orka.ai"] - resources: ["tasks/finalizers", "tools/finalizers", "agents/finalizers", "agentruntimes/finalizers", "providers/finalizers", "skills/finalizers", "repositorymonitors/finalizers", "repositoryscans/finalizers", "substrateactorpools/finalizers", "outboundaccesspolicies/finalizers"] + resources: ["tasks/finalizers", "tools/finalizers", "agents/finalizers", "agentruntimes/finalizers", "runtimepools/finalizers", "promptattempts/finalizers", "runtimesessioncontrols/finalizers", "branchclaims/finalizers", "publications/finalizers", "controllerepochs/finalizers", "externaleffects/finalizers", "providers/finalizers", "skills/finalizers", "repositorymonitors/finalizers", "repositoryscans/finalizers"] verbs: ["update"] - apiGroups: ["gateway.orka.ai"] resources: ["gatewayclasses", "gateways", "gatewaybindings"] @@ -31,26 +31,6 @@ rules: resourceNames: ["tasks.core.orka.ai", "gatewayclasses.gateway.orka.ai", "gateways.gateway.orka.ai", "gatewaybindings.gateway.orka.ai"] verbs: ["get"] - # Provider-neutral Execution Workspace control plane - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaceclasses"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "use"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaces"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "admit"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspacepools"] - verbs: ["get", "list", "watch"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaceproviders"] - verbs: ["get", "list", "watch", "update", "patch"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaceproviders/status", "executionworkspaceclasses/status", "executionworkspacepools/status", "executionworkspaces/status"] - verbs: ["get", "update", "patch"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaceproviders/finalizers", "executionworkspaceclasses/finalizers", "executionworkspaces/finalizers"] - verbs: ["update"] - # Job permissions - apiGroups: ["batch"] resources: ["jobs"] @@ -68,7 +48,7 @@ rules: verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["pods"] - verbs: ["get", "list", "watch"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] @@ -83,13 +63,16 @@ rules: verbs: ["create"] - apiGroups: [""] resources: ["namespaces"] - verbs: ["get", "list", "watch"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch", "create", "patch"] - apiGroups: [""] - resources: ["nodes", "services", "endpoints"] + resources: ["nodes", "endpoints"] verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["discovery.k8s.io"] resources: ["endpointslices"] verbs: ["get", "list", "watch"] @@ -99,14 +82,17 @@ rules: # Workload resource permissions (read-only, for chat K8s tools) - apiGroups: ["apps"] - resources: ["deployments", "replicasets", "statefulsets", "daemonsets"] + resources: ["deployments", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["apps"] + resources: ["statefulsets", "daemonsets"] verbs: ["get", "list", "watch"] - apiGroups: ["networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch"] - apiGroups: ["networking.k8s.io"] resources: ["networkpolicies"] - verbs: ["get", "list", "watch", "create", "delete"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Agent sandbox workspace backend: create/reattach/delete claims and connect to sandbox pods - apiGroups: ["extensions.agents.x-k8s.io"] resources: ["sandboxclaims"] @@ -129,7 +115,7 @@ rules: verbs: ["get", "list", "watch"] - apiGroups: ["policy"] resources: ["poddisruptionbudgets"] - verbs: ["get", "list", "watch"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] verbs: ["get", "list", "watch"] @@ -145,7 +131,7 @@ rules: resources: ["clusterroles", "roles", "rolebindings"] verbs: ["get", "list", "watch"] - apiGroups: ["rbac.authorization.k8s.io"] - resources: ["roles", "rolebindings"] + resources: ["rolebindings"] verbs: ["create", "update", "delete"] - apiGroups: ["rbac.authorization.k8s.io"] resources: ["clusterrolebindings"] @@ -175,13 +161,13 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} labels: {{- include "orka.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ include "orka.fullname" . }}-controller + name: {{ include "orka.controllerName" . }} subjects: - kind: ServiceAccount name: {{ include "orka.serviceAccountName" . }} @@ -304,7 +290,7 @@ rules: resources: ["secrets"] verbs: ["get", "create", "update", "delete"] - apiGroups: ["core.orka.ai"] - resources: ["tools", "outboundaccesspolicies"] + resources: ["tools"] verbs: ["get", "list"] - apiGroups: ["core.orka.ai"] resources: ["agents"] @@ -407,7 +393,7 @@ rules: resources: ["configmaps"] verbs: ["get", "list", "watch"] - apiGroups: ["core.orka.ai"] - resources: ["tools", "outboundaccesspolicies"] + resources: ["tools"] verbs: ["get", "list"] - apiGroups: ["core.orka.ai"] resources: ["agents"] @@ -528,48 +514,4 @@ subjects: - kind: ServiceAccount name: {{ include "orka.containerWorkerServiceAccountName" . }} namespace: {{ .Release.Namespace }} - ---- -# Adapter charts aggregate read-only access to their provider-specific parameter CRDs here. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "orka.fullname" . }}-workspace-parameter-reader - labels: - {{- include "orka.labels" . | nindent 4 }} -aggregationRule: - clusterRoleSelectors: - - matchLabels: - workspace.orka.ai/aggregate-to-parameter-reader: "true" -rules: [] -{{- if .Values.controller.workspaceProvider.fakeProviderEnabled }} ---- -# Development-only fake provider parameters contribute to the aggregate reader. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "orka.fullname" . }}-fake-workspace-parameter-reader - labels: - {{- include "orka.labels" . | nindent 4 }} - workspace.orka.ai/aggregate-to-parameter-reader: "true" -rules: - - apiGroups: ["fake.workspace.orka.ai"] - resources: ["fakeproviderconfigs", "fakepoolparameters"] - verbs: ["get", "list", "watch"] -{{- end }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "orka.fullname" . }}-workspace-parameter-reader - labels: - {{- include "orka.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "orka.fullname" . }}-workspace-parameter-reader -subjects: - - kind: ServiceAccount - name: {{ include "orka.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} {{- end }} diff --git a/cmd/build/helmify/static/templates/runtime-namespace.yaml b/cmd/build/helmify/static/templates/runtime-namespace.yaml new file mode 100644 index 000000000..db0051ef4 --- /dev/null +++ b/cmd/build/helmify/static/templates/runtime-namespace.yaml @@ -0,0 +1,13 @@ +{{- if .Values.controller.acpRuntime.enabled }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Values.controller.acpRuntime.namespace }} + labels: + app.kubernetes.io/name: {{ include "orka.name" . }} + app.kubernetes.io/component: acp-runtime + app.kubernetes.io/managed-by: {{ .Release.Service }} + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/audit: restricted +{{- end }} diff --git a/cmd/build/helmify/static/templates/scm-egress-proxy-deployment.yaml b/cmd/build/helmify/static/templates/scm-egress-proxy-deployment.yaml new file mode 100644 index 000000000..5451575af --- /dev/null +++ b/cmd/build/helmify/static/templates/scm-egress-proxy-deployment.yaml @@ -0,0 +1,85 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +{{- $secretName := .Values.scmEgressProxy.auth.existingSecret | default (include "orka.scmEgressProxyAuthSecretName" .) }} +{{- $tokenKey := .Values.scmEgressProxy.auth.tokenKey | default "token" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +spec: + replicas: 1 + strategy: {type: Recreate} + selector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: scm-egress-proxy + template: + metadata: + {{- with .Values.scmEgressProxy.auth.rolloutNonce }} + annotations: + orka.ai/scm-egress-proxy-auth-rollout-nonce: {{ . | quote }} + {{- end }} + labels: + {{- include "orka.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: scm-egress-proxy + orka.ai/network-role: scm-egress-proxy + spec: + serviceAccountName: {{ include "orka.scmEgressProxyName" . }} + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: {type: RuntimeDefault} + containers: + - name: proxy + image: {{ include "orka.imageRef" .Values.controller.image | quote }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + command: [/scm-egress-proxy] + args: + - --listen-address=:8080 + - {{ printf "--allowed-hosts=%s" .Values.publisher.allowedSCMHosts | quote }} + {{- if .Values.publisher.githubPR.enabled }} + - {{ printf "--forge-api-base-url=%s" .Values.publisher.githubPR.apiBaseURL | quote }} + {{- else }} + - --forge-api-base-url= + {{- end }} + - --token-file=/var/run/secrets/orka/scm-egress/token + - {{ printf "--max-request-header-bytes=%v" .Values.scmEgressProxy.maxRequestHeaderBytes | quote }} + - {{ printf "--max-response-header-bytes=%v" .Values.scmEgressProxy.maxResponseHeaderBytes | quote }} + - {{ printf "--max-request-bytes=%v" .Values.scmEgressProxy.maxRequestBytes | quote }} + - {{ printf "--max-response-bytes=%v" .Values.scmEgressProxy.maxResponseBytes | quote }} + - {{ printf "--max-tunnel-bytes=%v" .Values.scmEgressProxy.maxTunnelBytes | quote }} + - {{ printf "--max-concurrent=%v" .Values.scmEgressProxy.maxConcurrent | quote }} + - {{ printf "--resolution-timeout=%s" .Values.scmEgressProxy.resolutionTimeout | quote }} + - {{ printf "--connect-timeout=%s" .Values.scmEgressProxy.connectTimeout | quote }} + - {{ printf "--response-header-timeout=%s" .Values.scmEgressProxy.responseHeaderTimeout | quote }} + - {{ printf "--forward-timeout=%s" .Values.scmEgressProxy.forwardTimeout | quote }} + - {{ printf "--idle-timeout=%s" .Values.scmEgressProxy.idleTimeout | quote }} + - {{ printf "--tunnel-timeout=%s" .Values.scmEgressProxy.tunnelTimeout | quote }} + ports: + - {name: http-proxy, containerPort: 8080} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: + {{- toYaml .Values.scmEgressProxy.resources | nindent 12 }} + readinessProbe: + httpGet: {path: /readyz, port: http-proxy} + livenessProbe: + httpGet: {path: /healthz, port: http-proxy} + volumeMounts: + - {name: auth, mountPath: /var/run/secrets/orka/scm-egress/token, subPath: token, readOnly: true} + volumes: + - name: auth + secret: + secretName: {{ $secretName }} + defaultMode: 0440 + items: + - key: {{ $tokenKey }} + path: token +{{- end }} diff --git a/cmd/build/helmify/static/templates/scm-egress-proxy-networkpolicy.yaml b/cmd/build/helmify/static/templates/scm-egress-proxy-networkpolicy.yaml new file mode 100644 index 000000000..7050e6aa4 --- /dev/null +++ b/cmd/build/helmify/static/templates/scm-egress-proxy-networkpolicy.yaml @@ -0,0 +1,63 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: scm-egress-proxy + policyTypes: [Ingress, Egress] + ingress: + - from: + - podSelector: + matchLabels: + {{- include "orka.selectorLabels" . | nindent 14 }} + app.kubernetes.io/component: workspace-publisher + ports: [{protocol: TCP, port: 8080}] + egress: + - to: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: kube-system} + podSelector: + matchLabels: {k8s-app: kube-dns} + ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}] + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 0.0.0.0/8 + - 10.0.0.0/8 + - 100.64.0.0/10 + - 127.0.0.0/8 + - 169.254.0.0/16 + - 172.16.0.0/12 + - 192.0.0.0/24 + - 192.0.2.0/24 + - 192.168.0.0/16 + - 198.18.0.0/15 + - 198.51.100.0/24 + - 203.0.113.0/24 + - 224.0.0.0/4 + - 240.0.0.0/4 + ports: [{protocol: TCP, port: 443}] + - to: + - ipBlock: + cidr: ::/0 + except: + - ::/128 + - ::1/128 + - 64:ff9b::/96 + - 64:ff9b:1::/48 + - 100::/64 + - 2001::/32 + - 2001:db8::/32 + - 2002::/16 + - fc00::/7 + - fe80::/10 + - ff00::/8 + ports: [{protocol: TCP, port: 443}] +{{- end }} diff --git a/cmd/build/helmify/static/templates/scm-egress-proxy-secret.yaml b/cmd/build/helmify/static/templates/scm-egress-proxy-secret.yaml new file mode 100644 index 000000000..e27e9630a --- /dev/null +++ b/cmd/build/helmify/static/templates/scm-egress-proxy-secret.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled (not .Values.scmEgressProxy.auth.existingSecret) }} +{{- $name := include "orka.scmEgressProxyAuthSecretName" . }} +{{- $key := .Values.scmEgressProxy.auth.tokenKey | default "token" }} +{{- $explicit := .Values.scmEgressProxy.auth.token | default "" | toString }} +{{- if and $explicit (not (regexMatch "^[A-Za-z0-9._~-]{32,256}$" $explicit)) }} +{{- fail "scmEgressProxy.auth.token must contain 32-256 RFC 3986 unreserved characters" }} +{{- end }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name }} +{{- $existingValue := "" }} +{{- if and $existing (hasKey $existing.data $key) }}{{- $existingValue = (index $existing.data $key | b64dec) }}{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +type: Opaque +stringData: + {{ $key }}: {{ default (default (randAlphaNum 64) $existingValue) $explicit | quote }} +{{- end }} diff --git a/cmd/build/helmify/static/templates/scm-egress-proxy-service.yaml b/cmd/build/helmify/static/templates/scm-egress-proxy-service.yaml new file mode 100644 index 000000000..8821b4450 --- /dev/null +++ b/cmd/build/helmify/static/templates/scm-egress-proxy-service.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +spec: + selector: + {{- include "orka.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy + ports: + - {name: http-proxy, port: 8080, targetPort: http-proxy} +{{- end }} diff --git a/cmd/build/helmify/static/templates/scm-egress-proxy-serviceaccount.yaml b/cmd/build/helmify/static/templates/scm-egress-proxy-serviceaccount.yaml new file mode 100644 index 000000000..7d1e0fa27 --- /dev/null +++ b/cmd/build/helmify/static/templates/scm-egress-proxy-serviceaccount.yaml @@ -0,0 +1,10 @@ +{{- if and .Values.publisher.enabled .Values.scmEgressProxy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "orka.scmEgressProxyName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: scm-egress-proxy +automountServiceAccountToken: false +{{- end }} diff --git a/cmd/build/helmify/static/templates/serviceaccount.yaml b/cmd/build/helmify/static/templates/serviceaccount.yaml index e7c65e6b9..a494cdb5c 100644 --- a/cmd/build/helmify/static/templates/serviceaccount.yaml +++ b/cmd/build/helmify/static/templates/serviceaccount.yaml @@ -11,6 +11,21 @@ metadata: {{- end }} {{- end }} --- +{{- if and .Values.publisher.enabled .Values.publisher.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "orka.publisherServiceAccountName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: workspace-publisher + {{- with .Values.publisher.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: false +{{- end }} +--- {{- if .Values.client.create }} apiVersion: v1 kind: ServiceAccount @@ -48,11 +63,3 @@ metadata: {{- include "orka.labels" . | nindent 4 }} orka.ai/worker: "true" orka.ai/worker-trust: "container" ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "orka.harnessWrapperName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper diff --git a/cmd/build/helmify/static/templates/store-pvc.yaml b/cmd/build/helmify/static/templates/store-pvc.yaml index b13ba6e81..9463d9224 100644 --- a/cmd/build/helmify/static/templates/store-pvc.yaml +++ b/cmd/build/helmify/static/templates/store-pvc.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: {{ include "orka.fullname" . }}-store + name: {{ include "orka.storeName" . }} labels: {{- include "orka.labels" . | nindent 4 }} app.kubernetes.io/component: store diff --git a/cmd/build/helmify/static/templates/vekil-ingress-networkpolicy.yaml b/cmd/build/helmify/static/templates/vekil-ingress-networkpolicy.yaml new file mode 100644 index 000000000..390089289 --- /dev/null +++ b/cmd/build/helmify/static/templates/vekil-ingress-networkpolicy.yaml @@ -0,0 +1,24 @@ +{{- if .Values.providerProxy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "orka.vekilIngressPolicyName" . }} + namespace: vekil-system + labels: + {{- include "orka.labels" . | nindent 4 }} + app.kubernetes.io/component: provider-auth-proxy +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: vekil + policyTypes: [Ingress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: + orka.ai/network-role: provider-auth-proxy + ports: [{protocol: TCP, port: 1337}] +{{- end }} diff --git a/cmd/build/helmify/static/templates/workspace-class-use-policy.yaml b/cmd/build/helmify/static/templates/workspace-class-use-policy.yaml deleted file mode 100644 index 1a8c61ec7..000000000 --- a/cmd/build/helmify/static/templates/workspace-class-use-policy.yaml +++ /dev/null @@ -1,72 +0,0 @@ -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicy -metadata: - name: {{ include "orka.fullname" . }}-task-workspace-class-use - labels: - {{- include "orka.labels" . | nindent 4 }} -spec: - failurePolicy: Fail - matchConstraints: - resourceRules: - - apiGroups: ["core.orka.ai"] - apiVersions: ["v1alpha1"] - operations: ["CREATE", "UPDATE"] - resources: ["tasks"] - validations: - - expression: >- - !has(object.spec.execution) || - !has(object.spec.execution.workspace) || - !has(object.spec.execution.workspace.classRef) || - authorizer.group('workspace.orka.ai') - .resource('executionworkspaceclasses') - .namespace(request.namespace) - .name(object.spec.execution.workspace.classRef.name) - .check('use').allowed() - message: caller is not authorized to use the selected ExecutionWorkspaceClass - reason: Forbidden ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicyBinding -metadata: - name: {{ include "orka.fullname" . }}-task-workspace-class-use - labels: - {{- include "orka.labels" . | nindent 4 }} -spec: - policyName: {{ include "orka.fullname" . }}-task-workspace-class-use - validationActions: [Deny] ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicy -metadata: - name: {{ include "orka.fullname" . }}-tool-workspace-class-use - labels: - {{- include "orka.labels" . | nindent 4 }} -spec: - failurePolicy: Fail - matchConstraints: - resourceRules: - - apiGroups: ["core.orka.ai"] - apiVersions: ["v1alpha1"] - operations: ["CREATE", "UPDATE"] - resources: ["tools"] - validations: - - expression: >- - !has(object.spec.mcp) || - !has(object.spec.mcp.workspace) || - authorizer.group('workspace.orka.ai') - .resource('executionworkspaceclasses') - .namespace(request.namespace) - .name(object.spec.mcp.workspace.classRef.name) - .check('use').allowed() - message: caller is not authorized to use the selected ExecutionWorkspaceClass - reason: Forbidden ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicyBinding -metadata: - name: {{ include "orka.fullname" . }}-tool-workspace-class-use - labels: - {{- include "orka.labels" . | nindent 4 }} -spec: - policyName: {{ include "orka.fullname" . }}-tool-workspace-class-use - validationActions: [Deny] diff --git a/cmd/build/helmify/static/templates/workspace-class-use-webhook.yaml b/cmd/build/helmify/static/templates/workspace-class-use-webhook.yaml deleted file mode 100644 index 216d9678f..000000000 --- a/cmd/build/helmify/static/templates/workspace-class-use-webhook.yaml +++ /dev/null @@ -1,66 +0,0 @@ -{{- $workspaceProvider := .Values.controller.workspaceProvider | default dict -}} -{{- $classUseAdmission := $workspaceProvider.classUseAdmission | default dict -}} -{{- if $classUseAdmission.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: {{ include "orka.fullname" . }}-workspace-webhook - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: controller -spec: - ports: - - name: https - port: 443 - targetPort: webhook-server - protocol: TCP - selector: - {{- include "orka.selectorLabels" . | nindent 4 }} - app.kubernetes.io/component: controller ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingWebhookConfiguration -metadata: - name: {{ include "orka.fullname" . }}-workspace-class-use - labels: - {{- include "orka.labels" . | nindent 4 }} -webhooks: - - name: taskworkspaceclassuse.core.orka.ai - admissionReviewVersions: ["v1"] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: 10 - clientConfig: - service: - name: {{ include "orka.fullname" . }}-workspace-webhook - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-task-workspace-class-use - port: 443 - caBundle: {{ $classUseAdmission.caBundle | quote }} - rules: - - operations: ["CREATE", "UPDATE"] - apiGroups: ["core.orka.ai"] - apiVersions: ["v1alpha1"] - resources: ["tasks"] - scope: Namespaced - - name: toolworkspaceclassuse.core.orka.ai - admissionReviewVersions: ["v1"] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: 10 - clientConfig: - service: - name: {{ include "orka.fullname" . }}-workspace-webhook - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-tool-workspace-class-use - port: 443 - caBundle: {{ $classUseAdmission.caBundle | quote }} - rules: - - operations: ["CREATE", "UPDATE"] - apiGroups: ["core.orka.ai"] - apiVersions: ["v1alpha1"] - resources: ["tools"] - scope: Namespaced -{{- end }} diff --git a/cmd/build/helmify/static/templates/workspace-core-admission-policy.yaml b/cmd/build/helmify/static/templates/workspace-core-admission-policy.yaml deleted file mode 100644 index 47dd68f2a..000000000 --- a/cmd/build/helmify/static/templates/workspace-core-admission-policy.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicy -metadata: - name: {{ include "orka.fullname" . }}-executionworkspace-core-admission - labels: - {{- include "orka.labels" . | nindent 4 }} -spec: - failurePolicy: Fail - matchConstraints: - resourceRules: - - apiGroups: ["workspace.orka.ai"] - apiVersions: ["v1alpha1"] - operations: ["CREATE", "UPDATE"] - resources: ["executionworkspaces"] - validations: - - expression: >- - !has(object.spec.coreAdmission) || - (oldObject != null && has(oldObject.spec.coreAdmission) && - object.spec.coreAdmission.admittedGeneration == oldObject.spec.coreAdmission.admittedGeneration && - has(object.spec.coreAdmission.poolBinding) == has(oldObject.spec.coreAdmission.poolBinding) && - (!has(object.spec.coreAdmission.poolBinding) || - (object.spec.coreAdmission.poolBinding.name == oldObject.spec.coreAdmission.poolBinding.name && - object.spec.coreAdmission.poolBinding.uid == oldObject.spec.coreAdmission.poolBinding.uid && - object.spec.coreAdmission.poolBinding.generation == oldObject.spec.coreAdmission.poolBinding.generation && - has(object.spec.coreAdmission.poolBinding.profileHash) == has(oldObject.spec.coreAdmission.poolBinding.profileHash) && - (!has(object.spec.coreAdmission.poolBinding.profileHash) || - object.spec.coreAdmission.poolBinding.profileHash == oldObject.spec.coreAdmission.poolBinding.profileHash)))) || - authorizer.group('workspace.orka.ai') - .resource('executionworkspaces') - .namespace(request.namespace) - .name(object.metadata.name) - .check('admit').allowed() - message: only the Orka core controller may establish or advance ExecutionWorkspace core admission - reason: Forbidden ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicyBinding -metadata: - name: {{ include "orka.fullname" . }}-executionworkspace-core-admission - labels: - {{- include "orka.labels" . | nindent 4 }} -spec: - policyName: {{ include "orka.fullname" . }}-executionworkspace-core-admission - validationActions: [Deny] diff --git a/cmd/build/helmify/static/templates/workspace-crd-upgrade-guard.yaml b/cmd/build/helmify/static/templates/workspace-crd-upgrade-guard.yaml deleted file mode 100644 index 561ba7849..000000000 --- a/cmd/build/helmify/static/templates/workspace-crd-upgrade-guard.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- $workspaceProvider := .Values.controller.workspaceProvider | default dict -}} -{{- if $workspaceProvider.apiEnabled -}} -{{- $workspaceCRD := lookup "apiextensions.k8s.io/v1" "CustomResourceDefinition" "" "executionworkspaces.workspace.orka.ai" -}} -{{- $schemaReady := false -}} -{{- if $workspaceCRD -}} - {{- range $version := $workspaceCRD.spec.versions -}} - {{- if eq $version.name "v1alpha1" -}} - {{- $properties := $version.schema.openAPIV3Schema.properties.spec.properties -}} - {{- if hasKey $properties "coreAdmission" -}} - {{- $schemaReady = true -}} - {{- end -}} - {{- end -}} - {{- end -}} -{{- end -}} -{{- $schemaCheckOverridden := $workspaceProvider.crdUpgradeSchemaVerified | default false -}} -{{- if and (or .Release.IsUpgrade $workspaceCRD) (not $schemaReady) (not $schemaCheckOverridden) -}} - {{- fail "controller.workspaceProvider.apiEnabled requires the current workspace CRDs; apply them with `helm show crds | kubectl apply --server-side -f -`, or set controller.workspaceProvider.crdUpgradeSchemaVerified=true only for offline rendering after independent verification" -}} -{{- end -}} -{{- end -}} diff --git a/cmd/build/helmify/static/values.yaml b/cmd/build/helmify/static/values.yaml index b3aa75189..1aedb462d 100644 --- a/cmd/build/helmify/static/values.yaml +++ b/cmd/build/helmify/static/values.yaml @@ -9,6 +9,7 @@ controller: image: repository: ghcr.io/orka-agents/orka tag: "0.1.1" + digest: "" pullPolicy: IfNotPresent # Resource limits @@ -44,25 +45,37 @@ controller: # Max active tasks per namespace (0 = unlimited) maxTasksPerNamespace: 0 - # Provider-neutral workspace.orka.ai control plane (disabled by default during rollout). - # Before enabling it during a Helm upgrade, apply the current chart CRDs explicitly; - # Helm does not upgrade CRDs from the crds/ directory and the chart fails closed otherwise. - workspaceProvider: - # Enable generic provider/class/pool/workspace coordination controllers. - apiEnabled: false - # Offline helm template --is-upgrade cannot use lookup. Set this only after - # independently verifying/applying the current workspace CRD schema. - crdUpgradeSchemaVerified: false - # Enable the development-only fake provider adapter. - fakeProviderEnabled: false - # Fail-closed Kubernetes admission required whenever apiEnabled is true. - classUseAdmission: - enabled: false - # Existing TLS Secret with tls.crt and tls.key for the webhook Service DNS name. - existingSecret: "" - # Base64-encoded PEM CA bundle trusted by the API server. - caBundle: "" + # Operation-scoped ACP artifact transport stored on the controller PVC. + acpArtifact: + existingSecret: "" + secretKey: capability-secret + secret: "" + maxBytes: 536870912 + # Managed ACP core RuntimePools for built-in agent Tasks. + acpRuntime: + # The supported agent execution path is ACP-only; there is no legacy fallback. + enabled: false + namespace: orka-runtimes + # The chart-managed provider proxy always runs in the Helm release namespace. + # Leave empty or set exactly to .Release.Namespace; other values are rejected. + providerProxyNamespace: "" + # Configure each provider that should be available with an immutable + # repository@sha256: reference. Empty providers remain unavailable + # and Tasks selecting them fail closed. + codexImage: "" + claudeImage: "" + copilotImage: "" + # Planned controller upgrades use a same-binary preStop child to call a + # loopback-only coordinator. Keep terminationGracePeriodSeconds greater + # than timeout and triggerTimeout so SIGTERM shutdown still has headroom. + upgradeDrain: + enabled: true + port: 8083 + timeout: 5m + pollInterval: 1s + triggerTimeout: 5m15s + terminationGracePeriodSeconds: 360 # Provider-neutral external gateway plane. gateway: enabled: true @@ -133,16 +146,16 @@ controller: # Namespace assigned to authorized OIDC callers for namespace isolation. Empty uses controller default. namespace: "" - # Transaction-token authentication, scoped authorization, and optional vendor-neutral TTS exchange. + # Context-token authentication, scoped authorization, and optional kontxt TTS exchange. # Empty values keep the controller defaults / corresponding environment variables. contextToken: - # Profile for external context tokens (supported: transaction-token). + # Profile for external context tokens (supported: kontxt). profile: "" # Issuer URL expected in context tokens. Requires profile and audience when set. issuer: "" # Audience expected in context tokens. Requires profile and issuer when set. audience: "" - # Optional JWKS URL. For transaction-token, defaults to /.well-known/jwks.json. + # Optional JWKS URL. For kontxt, defaults to /.well-known/jwks.json. jwksUrl: "" # Comma-separated token headers, e.g. Txn-Token or Txn-Token,Authorization:Bearer. headers: "" @@ -158,7 +171,6 @@ controller: toolUse: "" providerUse: "" secretRead: "" - secretCredentialRead: "" agentRead: "" agentWrite: "" memoryRead: "" @@ -175,8 +187,8 @@ controller: gatewayRead: "" gatewayOperate: "" tts: - # Exact TTS OAuth endpoint for child/outbound transaction-token exchange. - endpoint: "" + # kontxt TTS base URL for child/outbound token exchange. + url: "" # Optional exchange audience. audience: "" # Exchange timeout, e.g. 5s. @@ -193,11 +205,127 @@ controller: childTokenTTL: "" toolTokenTTL: "" - outboundAccess: - # Exact namespace/name:port tuples trusted for cross-namespace gateway refs. - trustedGatewayServices: [] - # Exact namespace/name:port tuples trusted for cross-namespace token endpoint refs. - trustedTokenEndpointServices: [] +# Clean-room Workspace/Publisher service. +publisher: + enabled: true + image: + repository: ghcr.io/orka-agents/orka/workspace-publisher + tag: "0.1.1" + digest: "" + pullPolicy: IfNotPresent + allowedSCMHosts: github.com + githubPR: + enabled: true + apiBaseURL: https://api.github.com + requestTimeout: 15s + maxResponseBytes: 4194304 + auth: + existingSecret: "" + controllerTokenKey: controller-token + capabilitySecretKey: operation-capability-secret + controllerToken: "" + capabilitySecret: "" + # Non-secret revision marker. When rotating the publisher auth Secret, bump + # this value in the same Helm upgrade to restart controller and publisher. + rolloutNonce: "" + serviceAccount: + create: true + name: "" + annotations: {} + persistence: + size: 2Gi + storageClass: "" + resources: + requests: + cpu: 100m + memory: 256Mi + ephemeral-storage: 512Mi + limits: + cpu: "2" + memory: 2Gi + ephemeral-storage: 2Gi + +# Authenticated, exact-host HTTPS egress boundary for the Workspace/Publisher. +# The Publisher has no direct public 443 egress; Git and forge API traffic must +# use this proxy, which re-resolves and validates every outbound connection. +scmEgressProxy: + enabled: true + noProxy: localhost,127.0.0.1,::1,.svc,.cluster.local + maxRequestHeaderBytes: 32768 + maxResponseHeaderBytes: 65536 + maxRequestBytes: 4194304 + maxResponseBytes: 8388608 + maxTunnelBytes: 1073741824 + maxConcurrent: 8 + resolutionTimeout: 5s + connectTimeout: 10s + responseHeaderTimeout: 30s + forwardTimeout: 2m + idleTimeout: 30s + tunnelTimeout: 10m + auth: + existingSecret: "" + tokenKey: token + # Existing Secret tokens must use only RFC 3986 unreserved characters and + # be 32-256 bytes so they can be carried in authenticated proxy userinfo. + token: "" + # Non-secret revision marker. When rotating the SCM proxy auth Secret, bump + # this value in the same Helm upgrade to restart publisher and SCM proxy. + rolloutNonce: "" + resources: + requests: + cpu: 25m + memory: 32Mi + ephemeral-storage: 32Mi + limits: + cpu: 500m + memory: 256Mi + ephemeral-storage: 128Mi + +# Authenticated boundary in front of the otherwise unauthenticated Vekil +# service. ACP RuntimePods can reach only this Service, never Vekil directly. +providerProxy: + enabled: false + # Only this chart-supported Vekil Service endpoint is accepted. One trailing + # slash is normalized; alternate hosts, namespaces, and ports are rejected. + upstreamBaseURL: http://vekil.vekil-system.svc:1337 + # Mounted Secret files are reloaded atomically; no proxy Pod restart is + # required for token changes. Reload failures make readiness fail and disable + # all authenticated forwarding until both files are valid again. + tokenReloadInterval: 5s + # Maximum remaining lifetime accepted from the overlap token's absolute + # deadline. Periodic reloads and Pod restarts cannot extend it. Maximum: 24h. + previousTokenOverlap: 10m + # Optional operator-controlled restart input for binary/flag changes or + # recovery. Change it to force only the provider-auth-proxy Pod to roll. + rolloutNonce: "" + auth: + existingSecret: "" + tokenKey: token + token: "" + # Optional overlap key/value. Existing single-token Secrets remain valid + # when this key is absent. For proxy-first rotation, publish new as current + # and old here, wait for proxy reload, then roll controller/runtime pools. + # For controller-first use, first pre-stage new here while current remains + # old and verify it through the proxy; only then publish new as current and + # old here and roll controller/runtime pools. The pre-staged token covers a + # controller request that arrives before the proxy observes the role swap. + # Remove the old token after all workloads advertise the new generation. + # previousTokenValidUntil must be an absolute RFC3339/RFC3339Nano time no + # later than previousTokenOverlap from when the proxy loads it. + previousTokenKey: previous-token + previousToken: "" + previousTokenValidUntilKey: previous-token-valid-until + previousTokenValidUntil: "" + resources: + requests: + cpu: 25m + memory: 32Mi + ephemeral-storage: 32Mi + limits: + cpu: 250m + memory: 128Mi + ephemeral-storage: 128Mi # Worker configuration workers: @@ -212,21 +340,6 @@ workers: repository: ghcr.io/orka-agents/orka/general-worker tag: "0.1.1" - harnessWrapper: - image: - repository: ghcr.io/orka-agents/orka/agent-harness-wrapper - tag: "0.1.1" - pullPolicy: IfNotPresent - auth: - # Existing Secret containing the shared wrapper bearer token. - # When existingSecret and token are both empty, Helm generates a release-local token. - existingSecret: "" - tokenKey: token - token: "" - # Optional Codex CLI sandbox mode for the Codex adapter. - codexSandboxMode: "" - resources: {} - # GitHub webhook and label-trigger configuration. # Configure a repository webhook to POST to /webhooks/github with the secret below. github: @@ -307,7 +420,7 @@ annotations: {} store: path: /data/orka.db persistence: - enabled: true # durable gateway inbox/outbox and Session history require a PVC + enabled: true # ACP control records, gateway history, and Session state require durable RWO storage size: 1Gi storageClass: "" # use cluster default accessMode: ReadWriteOnce diff --git a/cmd/cli/helpers_test.go b/cmd/cli/helpers_test.go index 4971b9035..3bf903a96 100644 --- a/cmd/cli/helpers_test.go +++ b/cmd/cli/helpers_test.go @@ -611,7 +611,7 @@ func TestRootCmdIncludesCoverageCommands(t *testing.T) { cmd := newRootCmd() want := []string{ "provider", "tool", "session", "secret", "security", "monitor", - "memory", "auth", "models", "workspace", "substrate", + "memory", "auth", "models", "workspace", "runtime-pool", "agent-runtime", "substrate", } seen := map[string]bool{} for _, sub := range cmd.Commands() { diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 36a4c902e..28ea28813 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -74,6 +74,8 @@ func newRootCmd() *cobra.Command { cmd.AddCommand(newAuthCmd()) cmd.AddCommand(newModelsCmd()) cmd.AddCommand(newWorkspaceCmd()) + cmd.AddCommand(newRuntimePoolCmd()) + cmd.AddCommand(newAgentRuntimeCmd()) cmd.AddCommand(newSubstrateCmd()) cmd.AddCommand(newGatewayCmd()) diff --git a/cmd/cli/misc_commands.go b/cmd/cli/misc_commands.go index 1656b95a1..d3ae7581f 100644 --- a/cmd/cli/misc_commands.go +++ b/cmd/cli/misc_commands.go @@ -91,36 +91,34 @@ func safeWorkspaceStatus(task client.TaskDetail) map[string]any { "task": client.StringField(task, "metadata", "name"), "namespace": client.StringField(task, "metadata", "namespace"), } - status, _ := task["status"].(map[string]any) - if status == nil { - return out - } + status := nestedMap(task, "status") out["phase"] = status["phase"] - if ew, ok := status["executionWorkspace"].(map[string]any); ok { - safe := map[string]any{} - for _, key := range []string{ - "phase", - "provider", - "reason", - "message", - "url", - "lastTransitionTime", - "observedGeneration", - } { - if v, ok := ew[key]; ok { - safe[key] = v - } - } - out["executionWorkspace"] = safe - } - if ws, ok := status["workspace"].(map[string]any); ok { - safe := map[string]any{} - for _, key := range []string{"phase", "provider", "reason", "message", "url", "lastTransitionTime"} { - if v, ok := ws[key]; ok { - safe[key] = v - } - } + + spec := nestedMap(task, "spec") + workspace := nestedMap(spec, "workspace") + if len(workspace) > 0 { + safe := copyKeys(workspace, + "intent", "gitRepo", "sourceRepository", "branch", "ref", "subPath", + "publicationGitRepo", "publicationRepository", "pushBranch", "prBaseBranch", "createPR") + safe["readCredentialConfigured"] = len(nestedMap(workspace, "readCredentialRef")) > 0 + safe["publicationReadCredentialConfigured"] = len(nestedMap(workspace, "publicationReadCredentialRef")) > 0 + publicationWriteConfigured := len(nestedMap(workspace, "publicationCredentialRef")) > 0 + // Preserve the existing summary field while making the write-only role explicit. + safe["publicationCredentialConfigured"] = publicationWriteConfigured + safe["publicationWriteCredentialConfigured"] = publicationWriteConfigured + safe["forgeCredentialConfigured"] = len(nestedMap(workspace, "forgeCredentialRef")) > 0 out["workspace"] = safe } + if executionWorkspace := nestedMap(status, "executionWorkspace"); len(executionWorkspace) > 0 { + out["executionWorkspace"] = copyKeys(executionWorkspace, + "phase", "provider", "reason", "message", "reusePolicy", "cleanupPolicy", "reused", + "placement", "density", "resumeLatency", "lastUpdateTime") + } + if delivery := nestedMap(status, "delivery"); len(delivery) > 0 { + out["delivery"] = copyKeys(delivery, + "state", "outcome", "reason", "publicationID", "sourceRepository", "publicationRepository", + "branch", "expectedCommitSHA", "verifiedRemoteSHA", "supersedingRemoteSHA", "artifactDigest", + "prReceipt", "message", "lastTransitionTime") + } return out } diff --git a/cmd/cli/resource_commands.go b/cmd/cli/resource_commands.go index 560b2ffe9..9771918e4 100644 --- a/cmd/cli/resource_commands.go +++ b/cmd/cli/resource_commands.go @@ -11,17 +11,18 @@ import ( ) type crudResourceSpec struct { - Use string - Short string - BasePath string - Name string - ReadOnly bool - NoGet bool - NoCreate bool - NoUpdate bool - NoDelete bool - ListFlags func(*cobra.Command) - ListQuery func(*cobra.Command) map[string]string + Use string + Short string + BasePath string + Name string + ReadOnly bool + NoGet bool + NoCreate bool + NoUpdate bool + NoDelete bool + ListFlags func(*cobra.Command) + ListQuery func(*cobra.Command) map[string]string + TablePrinter func(*cobra.Command, any) error } func newCRUDResourceCmd(spec crudResourceSpec) *cobra.Command { @@ -69,6 +70,13 @@ func newCRUDListCmd(spec crudResourceSpec) *cobra.Command { if err != nil { return err } + format, err := outputFormat(cmd) + if err != nil { + return err + } + if format == outputTable && spec.TablePrinter != nil { + return spec.TablePrinter(cmd, result) + } return printStructured(cmd, result) }, } @@ -93,6 +101,13 @@ func newCRUDGetCmd(spec crudResourceSpec) *cobra.Command { if err != nil { return err } + format, err := outputFormat(cmd) + if err != nil { + return err + } + if format == outputTable && spec.TablePrinter != nil { + return spec.TablePrinter(cmd, result) + } return printStructured(cmd, result) }, } diff --git a/cmd/cli/runtime.go b/cmd/cli/runtime.go new file mode 100644 index 000000000..bb5585ffc --- /dev/null +++ b/cmd/cli/runtime.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "text/tabwriter" + + "github.com/spf13/cobra" +) + +func newRuntimePoolCmd() *cobra.Command { + return newCRUDResourceCmd(crudResourceSpec{ + Use: "runtime-pool", + Short: "Manage controller-owned ACP runtime pools", + BasePath: "/api/v1/runtime-pools", + Name: "runtime pool", + ReadOnly: true, + TablePrinter: printRuntimePoolTable, + }) +} + +func newAgentRuntimeCmd() *cobra.Command { + return newCRUDResourceCmd(crudResourceSpec{ + Use: "agent-runtime", + Short: "Manage external orka.harness.v2 AgentRuntime registrations", + BasePath: "/api/v1/agent-runtimes", + Name: "agent runtime", + TablePrinter: printAgentRuntimeTable, + }) +} + +func printRuntimePoolTable(cmd *cobra.Command, value any) error { + items := listItems(value) + if len(items) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No runtime pools found.") //nolint:errcheck + return nil + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "NAME\tNAMESPACE\tLIFECYCLE\tADMISSION\tPODS\tSESSIONS\tPROMPTS\tQUEUED\tAGE") //nolint:errcheck + for _, item := range items { + status := nestedMap(item, "status") + capacity := nestedMap(status, "capacity") + pods := fmt.Sprintf("%s/%s", dash(anyString(status["currentReplicas"])), dash(anyString(status["desiredReplicas"]))) + sessions := fmt.Sprintf("%s/%s", dash(anyString(capacity["residentSessions"])), dash(anyString(capacity["maxResidentSessions"]))) + prompts := fmt.Sprintf("%s/%s", dash(anyString(capacity["runningPrompts"])), dash(anyString(capacity["maxRunningPrompts"]))) + fmt.Fprintf( //nolint:errcheck + w, + "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + dash(nestedString(item, "metadata", "name")), + dash(nestedString(item, "metadata", "namespace")), + dash(anyString(status["lifecycle"])), + dash(anyString(status["admissionState"])), + pods, + sessions, + prompts, + dash(anyString(capacity["queuedTasks"])), + dash(formatAge(nestedString(item, "metadata", "creationTimestamp"))), + ) //nolint:errcheck + } + return w.Flush() +} + +func printAgentRuntimeTable(cmd *cobra.Command, value any) error { + items := listItems(value) + if len(items) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No agent runtimes found.") //nolint:errcheck + return nil + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "NAME\tNAMESPACE\tREADY\tCONTRACT\tINTENT\tPROVIDER/MODEL\tINSTANCE\tAGE") //nolint:errcheck + for _, item := range items { + spec := nestedMap(item, "spec") + status := nestedMap(item, "status") + capabilities := nestedMap(spec, "capabilities") + profile := nestedMap(capabilities, "profile") + observed := nestedMap(status, "observedCapabilities") + provider := firstString(observed, "providerKind") + if provider == "" { + provider = firstString(profile, "providerKind") + } + model := firstString(observed, "model") + if model == "" { + model = firstString(profile, "model") + } + instance := firstString(observed, "runtimeInstanceID") + if instance == "" { + instance = firstString(capabilities, "runtimeInstanceID") + } + fmt.Fprintf( //nolint:errcheck + w, + "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + dash(nestedString(item, "metadata", "name")), + dash(nestedString(item, "metadata", "namespace")), + dash(anyString(status["ready"])), + dash(anyString(spec["contractVersion"])), + dash(anyString(profile["workspaceIntent"])), + dash(joinNonEmpty(provider, model, "/")), + dash(compactCLIValue(instance)), + dash(formatAge(nestedString(item, "metadata", "creationTimestamp"))), + ) //nolint:errcheck + } + return w.Flush() +} + +func nestedMap(m map[string]any, keys ...string) map[string]any { + current := m + for _, key := range keys { + next, ok := current[key].(map[string]any) + if !ok { + return map[string]any{} + } + current = next + } + return current +} + +func joinNonEmpty(left, right, separator string) string { + if left == "" { + return right + } + if right == "" { + return left + } + return left + separator + right +} + +func compactCLIValue(value string) string { + const max = 30 + if len(value) <= max { + return value + } + return value[:17] + "…" + value[len(value)-8:] +} diff --git a/cmd/cli/runtime_test.go b/cmd/cli/runtime_test.go new file mode 100644 index 000000000..7a7b5928e --- /dev/null +++ b/cmd/cli/runtime_test.go @@ -0,0 +1,91 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestRuntimePoolListRendersCapacityAndAdmission(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/runtime-pools" { + t.Fatalf("path = %s", r.URL.Path) + } + json.NewEncoder(w).Encode(map[string]any{ //nolint:errcheck + "items": []map[string]any{{ + "metadata": map[string]any{"name": "codex-read", "namespace": "default"}, + "status": map[string]any{ + "lifecycle": "Serving", "admissionState": "Accepting", "currentReplicas": 1, "desiredReplicas": 1, + "capacity": map[string]any{"residentSessions": 3, "maxResidentSessions": 10, "runningPrompts": 2, "maxRunningPrompts": 4, "queuedTasks": 1}, + }, + }}, + }) + })) + defer server.Close() + + var out bytes.Buffer + root := newRootCmd() + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"--server", server.URL, "--token", "test-token", "runtime-pool", "list"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + for _, want := range []string{"codex-read", "Serving", "Accepting", "3/10", "2/4"} { + if !strings.Contains(out.String(), want) { + t.Fatalf("output missing %q:\n%s", want, out.String()) + } + } +} + +func TestAgentRuntimeListRendersOnlyV2Identity(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{ //nolint:errcheck + "items": []map[string]any{{ + "metadata": map[string]any{"name": "external-codex", "namespace": "default"}, + "spec": map[string]any{ + "contractVersion": "orka.harness.v2", + "capabilities": map[string]any{ + "runtimeInstanceID": "external-instance-1", + "profile": map[string]any{"workspaceIntent": "read", "providerKind": "openai", "model": "gpt-5"}, + }, + }, + "status": map[string]any{"ready": true}, + }}, + }) + })) + defer server.Close() + + var out bytes.Buffer + root := newRootCmd() + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"--server", server.URL, "--token", "test-token", "agent-runtime", "list"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + for _, want := range []string{"external-codex", "true", "orka.harness.v2", "read", "openai/gpt-5"} { + if !strings.Contains(out.String(), want) { + t.Fatalf("output missing %q:\n%s", want, out.String()) + } + } + for _, legacy := range []string{"supportsContinuation", "toolExecutionModes", "brokeredToolClasses"} { + if strings.Contains(out.String(), legacy) { + t.Fatalf("output contains legacy field %q: %s", legacy, out.String()) + } + } +} + +func TestRuntimePoolCommandIsReadOnly(t *testing.T) { + cmd := newRuntimePoolCmd() + for _, child := range cmd.Commands() { + switch child.Name() { + case "list", "get": + default: + t.Fatalf("unexpected mutating RuntimePool command %q", child.Name()) + } + } +} diff --git a/cmd/cli/task.go b/cmd/cli/task.go index 4a3edbb78..f350f62e9 100644 --- a/cmd/cli/task.go +++ b/cmd/cli/task.go @@ -39,6 +39,7 @@ func newTaskCmd() *cobra.Command { cmd.AddCommand(newTaskCreateCmd()) cmd.AddCommand(newTaskListCmd()) cmd.AddCommand(newTaskGetCmd()) + cmd.AddCommand(newTaskRuntimeStatusCmd()) cmd.AddCommand(newTaskLogsCmd()) cmd.AddCommand(newTaskEventsCmd()) cmd.AddCommand(newTaskFollowCmd()) @@ -62,6 +63,7 @@ func newTaskCreateCmd() *cobra.Command { var commandVals, argVals, envVals []string var priority int32 var suspend bool + var workspaceOptions taskWorkspaceCreateOptions cmd := &cobra.Command{ Use: "create ", @@ -155,7 +157,26 @@ func newTaskCreateCmd() *cobra.Command { } } - result, err := c.CreateTask(context.Background(), req) + workspace, err := workspaceOptions.build(cmd, taskType) + if err != nil { + return err + } + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("marshal task request: %w", err) + } + if workspace != nil { + var payload map[string]any + if err := json.Unmarshal(body, &payload); err != nil { + return fmt.Errorf("prepare task workspace request: %w", err) + } + payload["workspace"] = workspace + body, err = json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal task workspace request: %w", err) + } + } + result, err := c.CreateTaskRaw(context.Background(), body) if err != nil { return err } @@ -186,6 +207,7 @@ func newTaskCreateCmd() *cobra.Command { cmd.Flags().StringVar(&schedule, "schedule", "", "Cron schedule for recurring tasks") cmd.Flags().StringVar(&timezone, "timezone", "", "IANA time zone for scheduled tasks") cmd.Flags().BoolVar(&suspend, "suspend", false, "Suspend scheduled task runs") + workspaceOptions.bindFlags(cmd) return cmd } diff --git a/cmd/cli/task_test.go b/cmd/cli/task_test.go index 7afe36cf7..b50ca2b08 100644 --- a/cmd/cli/task_test.go +++ b/cmd/cli/task_test.go @@ -62,7 +62,7 @@ func TestNewTaskCmd(t *testing.T) { for _, sub := range cmd.Commands() { subNames[sub.Use] = true } - for _, want := range []string{"create ", "list", "get ", "logs ", "delete "} { + for _, want := range []string{"create ", "list", "get ", "status ", "logs ", "delete "} { if !subNames[want] { t.Errorf("missing subcommand %q", want) } @@ -73,7 +73,13 @@ func TestNewTaskCreateCmdFlags(t *testing.T) { cmd := newTaskCreateCmd() // Verify flags - for _, flagName := range []string{"type", "agent", "provider", "timeout"} { + for _, flagName := range []string{ + "type", "agent", "provider", "timeout", "workspace-intent", "git-repo", + "read-credential", "read-credential-key", "publication-git-repo", + "publication-read-credential", "publication-read-credential-key", + "publication-credential", "publication-credential-key", "forge-credential", "forge-credential-key", + "push-branch", "create-pr", + } { if cmd.Flags().Lookup(flagName) == nil { t.Errorf("missing flag %q", flagName) } diff --git a/cmd/cli/task_workspace.go b/cmd/cli/task_workspace.go new file mode 100644 index 000000000..3145794af --- /dev/null +++ b/cmd/cli/task_workspace.go @@ -0,0 +1,269 @@ +package main + +import ( + "fmt" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + + corev1alpha1 "github.com/orka-agents/orka/api/v1alpha1" + "github.com/orka-agents/orka/internal/cli/client" +) + +type taskWorkspaceCreateOptions struct { + intent string + gitRepo string + sourceRepositoryProvider string + sourceRepositoryID string + branch string + ref string + subPath string + readCredential string + readCredentialKey string + publicationGitRepo string + publicationRepositoryProvider string + publicationRepositoryID string + publicationReadCredential string + publicationReadCredentialKey string + publicationCredential string + publicationCredentialKey string + forgeCredential string + forgeCredentialKey string + pushBranch string + prBaseBranch string + createPR bool +} + +func (o *taskWorkspaceCreateOptions) bindFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.intent, "workspace-intent", "read", "Agent workspace intent: read or write") + cmd.Flags().StringVar(&o.gitRepo, "git-repo", "", "Source repository URL (credentials must not be embedded)") + cmd.Flags().StringVar(&o.sourceRepositoryProvider, "source-repository-provider", "", "Canonical source repository provider") + cmd.Flags().StringVar(&o.sourceRepositoryID, "source-repository-id", "", "Canonical source repository ID") + cmd.Flags().StringVar(&o.branch, "branch", "", "Source branch") + cmd.Flags().StringVar(&o.ref, "ref", "", "Source commit, tag, or ref") + cmd.Flags().StringVar(&o.subPath, "sub-path", "", "Subdirectory within the source repository") + cmd.Flags().StringVar(&o.readCredential, "read-credential", "", "Secret name for source clone/read credentials") + cmd.Flags().StringVar(&o.readCredentialKey, "read-credential-key", "", "Secret key for source clone/read credentials (default: token)") + cmd.Flags().StringVar(&o.publicationGitRepo, "publication-git-repo", "", "Publication repository URL") + cmd.Flags().StringVar(&o.publicationRepositoryProvider, "publication-repository-provider", "", "Canonical publication repository provider") + cmd.Flags().StringVar(&o.publicationRepositoryID, "publication-repository-id", "", "Canonical publication repository ID") + cmd.Flags().StringVar(&o.publicationReadCredential, "publication-read-credential", "", "Secret name for publication preflight and verification credentials") + cmd.Flags().StringVar(&o.publicationReadCredentialKey, "publication-read-credential-key", "", "Secret key for publication preflight and verification credentials (default: token)") + cmd.Flags().StringVar(&o.publicationCredential, "publication-credential", "", "Secret name for publication write credentials") + cmd.Flags().StringVar(&o.publicationCredentialKey, "publication-credential-key", "", "Secret key for publication write credentials (default: token)") + cmd.Flags().StringVar(&o.forgeCredential, "forge-credential", "", "Secret name for forge API credentials used to reconcile pull requests") + cmd.Flags().StringVar(&o.forgeCredentialKey, "forge-credential-key", "", "Secret key for forge API credentials (default: token)") + cmd.Flags().StringVar(&o.pushBranch, "push-branch", "", "Publication branch (default: controller-derived full-entropy branch)") + cmd.Flags().StringVar(&o.prBaseBranch, "pr-base-branch", "", "Pull request base branch") + cmd.Flags().BoolVar(&o.createPR, "create-pr", false, "Reconcile a pull request after verified publication") +} + +func (o taskWorkspaceCreateOptions) build(cmd *cobra.Command, taskType string) (map[string]any, error) { + intent := strings.ToLower(strings.TrimSpace(o.intent)) + if intent != string(corev1alpha1.WorkspaceIntentRead) && intent != string(corev1alpha1.WorkspaceIntentWrite) { + return nil, fmt.Errorf("--workspace-intent must be read or write") + } + workspaceFlagsUsed := false + for _, name := range []string{ + "workspace-intent", "git-repo", "source-repository-provider", "source-repository-id", "branch", "ref", + "sub-path", "read-credential", "read-credential-key", "publication-git-repo", "publication-repository-provider", + "publication-repository-id", "publication-read-credential", "publication-read-credential-key", + "publication-credential", "publication-credential-key", "forge-credential", "forge-credential-key", + "push-branch", "pr-base-branch", "create-pr", + } { + workspaceFlagsUsed = workspaceFlagsUsed || cmd.Flags().Changed(name) + } + if taskType != cliTaskTypeAgent { + if workspaceFlagsUsed { + return nil, fmt.Errorf("workspace flags are supported only for agent tasks") + } + return nil, nil + } + if (strings.TrimSpace(o.sourceRepositoryProvider) == "") != (strings.TrimSpace(o.sourceRepositoryID) == "") { + return nil, fmt.Errorf("--source-repository-provider and --source-repository-id must be set together") + } + if (strings.TrimSpace(o.publicationRepositoryProvider) == "") != (strings.TrimSpace(o.publicationRepositoryID) == "") { + return nil, fmt.Errorf("--publication-repository-provider and --publication-repository-id must be set together") + } + for _, credential := range []struct { + nameFlag string + name string + keyFlag string + key string + }{ + {nameFlag: "--read-credential", name: o.readCredential, keyFlag: "--read-credential-key", key: o.readCredentialKey}, + {nameFlag: "--publication-read-credential", name: o.publicationReadCredential, keyFlag: "--publication-read-credential-key", key: o.publicationReadCredentialKey}, + {nameFlag: "--publication-credential", name: o.publicationCredential, keyFlag: "--publication-credential-key", key: o.publicationCredentialKey}, + {nameFlag: "--forge-credential", name: o.forgeCredential, keyFlag: "--forge-credential-key", key: o.forgeCredentialKey}, + } { + if strings.TrimSpace(credential.key) != "" && strings.TrimSpace(credential.name) == "" { + return nil, fmt.Errorf("%s requires %s", credential.keyFlag, credential.nameFlag) + } + } + publicationRequested := o.createPR || strings.TrimSpace(o.publicationGitRepo) != "" || + strings.TrimSpace(o.publicationRepositoryProvider) != "" || strings.TrimSpace(o.publicationReadCredential) != "" || + strings.TrimSpace(o.publicationReadCredentialKey) != "" || strings.TrimSpace(o.publicationCredential) != "" || + strings.TrimSpace(o.publicationCredentialKey) != "" || strings.TrimSpace(o.forgeCredential) != "" || + strings.TrimSpace(o.forgeCredentialKey) != "" || strings.TrimSpace(o.pushBranch) != "" || strings.TrimSpace(o.prBaseBranch) != "" + if err := o.validatePublicationOptions(intent, publicationRequested); err != nil { + return nil, err + } + + workspace := map[string]any{"intent": intent} + addTrimmed(workspace, "gitRepo", o.gitRepo) + addRepositoryIdentity(workspace, "sourceRepository", o.sourceRepositoryProvider, o.sourceRepositoryID) + addTrimmed(workspace, "branch", o.branch) + addTrimmed(workspace, "ref", o.ref) + addTrimmed(workspace, "subPath", o.subPath) + addCredentialRef(workspace, "readCredentialRef", o.readCredential, o.readCredentialKey) + if intent == string(corev1alpha1.WorkspaceIntentWrite) { + addTrimmed(workspace, "publicationGitRepo", o.publicationGitRepo) + addRepositoryIdentity(workspace, "publicationRepository", o.publicationRepositoryProvider, o.publicationRepositoryID) + addCredentialRef(workspace, "publicationReadCredentialRef", o.publicationReadCredential, o.publicationReadCredentialKey) + addCredentialRef(workspace, "publicationCredentialRef", o.publicationCredential, o.publicationCredentialKey) + addCredentialRef(workspace, "forgeCredentialRef", o.forgeCredential, o.forgeCredentialKey) + addTrimmed(workspace, "pushBranch", o.pushBranch) + addTrimmed(workspace, "prBaseBranch", o.prBaseBranch) + if o.createPR { + workspace["createPR"] = true + } + } + return workspace, nil +} + +func (o taskWorkspaceCreateOptions) validatePublicationOptions(intent string, publicationRequested bool) error { + if intent != string(corev1alpha1.WorkspaceIntentWrite) { + if publicationRequested { + return fmt.Errorf("publication flags require --workspace-intent write") + } + return nil + } + if strings.TrimSpace(o.gitRepo) == "" { + return fmt.Errorf("--workspace-intent write requires --git-repo") + } + if strings.TrimSpace(o.publicationCredential) == "" { + return fmt.Errorf("--workspace-intent write requires --publication-credential") + } + if o.createPR && strings.TrimSpace(o.prBaseBranch) == "" { + return fmt.Errorf("--create-pr requires --pr-base-branch") + } + if o.createPR && strings.TrimSpace(o.forgeCredential) == "" { + return fmt.Errorf("--create-pr requires --forge-credential") + } + return nil +} + +func addTrimmed(target map[string]any, key, value string) { + if value = strings.TrimSpace(value); value != "" { + target[key] = value + } +} + +func addRepositoryIdentity(target map[string]any, key, provider, id string) { + provider = strings.TrimSpace(provider) + id = strings.TrimSpace(id) + if provider != "" && id != "" { + target[key] = map[string]any{"provider": provider, "id": id} + } +} + +func addCredentialRef(target map[string]any, field, name, secretKey string) { + name = strings.TrimSpace(name) + if name == "" { + return + } + ref := map[string]any{"name": name} + if secretKey = strings.TrimSpace(secretKey); secretKey != "" { + ref["key"] = secretKey + } + target[field] = ref +} + +func newTaskRuntimeStatusCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "status ", + Short: "Show durable execution, delivery, and runtime-pool status", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c := newClientFromCmd(cmd) + detail, err := c.GetTask(cmd.Context(), args[0], client.GetOptions{Namespace: c.Namespace}) + if err != nil { + return err + } + status := safeTaskRuntimeStatus(*detail) + format, err := outputFormat(cmd) + if err != nil { + return err + } + if format != outputTable { + return printStructured(cmd, status) + } + return printTaskRuntimeStatusTable(cmd, status) + }, + } + addOutputFlag(cmd, outputTable) + return cmd +} + +func safeTaskRuntimeStatus(task client.TaskDetail) map[string]any { + out := map[string]any{ + "task": client.StringField(task, "metadata", "name"), + "namespace": client.StringField(task, "metadata", "namespace"), + } + status := nestedMap(task, "status") + out["phase"] = status["phase"] + if execution := nestedMap(status, "execution"); len(execution) > 0 { + out["execution"] = copyKeys(execution, + "state", "outcome", "reason", "attempt", "promptID", "runtimePoolName", "runtimePoolUID", + "runtimeInstanceID", "runtimeSessionUID", "runtimeSessionGeneration", "requestDigest", "controllerEpoch", + "message", "lastTransitionTime") + } + if delivery := nestedMap(status, "delivery"); len(delivery) > 0 { + out["delivery"] = copyKeys(delivery, + "state", "outcome", "reason", "publicationID", "sourceRepository", "publicationRepository", "branch", + "startingSHA", "remoteBeforeSHA", "treeSHA", "expectedCommitSHA", "verifiedRemoteSHA", "supersedingRemoteSHA", + "artifactDigest", "prReceipt", "message", "lastTransitionTime") + } + return out +} + +func printTaskRuntimeStatusTable(cmd *cobra.Command, status map[string]any) error { + execution := nestedMap(status, "execution") + delivery := nestedMap(status, "delivery") + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "FIELD\tVALUE") //nolint:errcheck + rows := [][2]string{ + {"Task", anyString(status["task"])}, + {"Namespace", anyString(status["namespace"])}, + {"Phase", anyString(status["phase"])}, + {"Execution", anyString(execution["state"])}, + {"Execution outcome", anyString(execution["outcome"])}, + {"Execution reason", anyString(execution["reason"])}, + {"Attempt", anyString(execution["attempt"])}, + {"RuntimePool", anyString(execution["runtimePoolName"])}, + {"Runtime instance", compactCLIValue(anyString(execution["runtimeInstanceID"]))}, + {"Runtime session generation", anyString(execution["runtimeSessionGeneration"])}, + {"Delivery", anyString(delivery["state"])}, + {"Delivery outcome", anyString(delivery["outcome"])}, + {"Publication branch", anyString(delivery["branch"])}, + {"Verified remote", compactCLIValue(anyString(delivery["verifiedRemoteSHA"]))}, + } + for _, row := range rows { + fmt.Fprintf(w, "%s\t%s\n", row[0], dash(row[1])) //nolint:errcheck + } + if execution["state"] == "OutcomeUnknown" || execution["outcome"] == "OutcomeUnknown" { + fmt.Fprintln(w, "Replay policy\tTerminal; create a new Task explicitly. No automatic replay.") //nolint:errcheck + } + return w.Flush() +} + +func copyKeys(source map[string]any, keys ...string) map[string]any { + out := map[string]any{} + for _, key := range keys { + if value, ok := source[key]; ok { + out[key] = value + } + } + return out +} diff --git a/cmd/cli/task_workspace_test.go b/cmd/cli/task_workspace_test.go new file mode 100644 index 000000000..69b140587 --- /dev/null +++ b/cmd/cli/task_workspace_test.go @@ -0,0 +1,249 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestTaskCreateWritesCanonicalWorkspaceCredentialRoles(t *testing.T) { + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]any{"metadata": map[string]any{"name": "write-task"}}) //nolint:errcheck + })) + defer server.Close() + + root := newRootCmd() + root.SetArgs([]string{ + "--server", server.URL, "--token", "test-token", "--namespace", "default", + "task", "create", "Update the repository", "--type", "agent", "--agent", "codex-agent", "--name", "write-task", + "--workspace-intent", "write", "--git-repo", "https://github.com/source/repo", + "--source-repository-provider", "github", "--source-repository-id", "source-id", + "--read-credential", "repo-read", "--read-credential-key", "source-token", + "--publication-git-repo", "https://github.com/publish/repo", "--publication-repository-provider", "github", + "--publication-repository-id", "publish-id", + "--publication-read-credential", "repo-verify", "--publication-read-credential-key", "verify-token", + "--publication-credential", "repo-write", "--publication-credential-key", "write-token", + "--forge-credential", "repo-forge", "--forge-credential-key", "forge-token", + "--push-branch", "orka/change", "--pr-base-branch", "main", "--create-pr", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + workspace := nestedMap(body, "workspace") + if got := anyString(workspace["intent"]); got != "write" { + t.Fatalf("intent = %q", got) + } + for _, want := range []struct { + field string + name string + key string + }{ + {field: "readCredentialRef", name: "repo-read", key: "source-token"}, + {field: "publicationReadCredentialRef", name: "repo-verify", key: "verify-token"}, + {field: "publicationCredentialRef", name: "repo-write", key: "write-token"}, + {field: "forgeCredentialRef", name: "repo-forge", key: "forge-token"}, + } { + if got := nestedString(workspace, want.field, "name"); got != want.name { + t.Errorf("%s name = %q, want %q", want.field, got, want.name) + } + if got := nestedString(workspace, want.field, "key"); got != want.key { + t.Errorf("%s key = %q, want %q", want.field, got, want.key) + } + } + if got := anyString(workspace["createPR"]); got != "true" { + t.Fatalf("createPR = %q", got) + } + if _, legacy := nestedMap(body, "agentRuntime")["workspace"]; legacy { + t.Fatal("request contains deprecated agentRuntime.workspace") + } + if _, legacy := workspace["gitSecretRef"]; legacy { + t.Fatal("request contains deprecated gitSecretRef") + } +} + +func TestTaskCreateLeavesCredentialKeyOmittedForAPIDefault(t *testing.T) { + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]any{"metadata": map[string]any{"name": "read-task"}}) //nolint:errcheck + })) + defer server.Close() + + root := newRootCmd() + root.SetArgs([]string{ + "--server", server.URL, "--token", "test-token", + "task", "create", "Inspect", "--type", "agent", "--agent", "a", "--read-credential", "repo-read", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + ref := nestedMap(nestedMap(body, "workspace"), "readCredentialRef") + if ref["name"] != "repo-read" { + t.Fatalf("readCredentialRef = %#v", ref) + } + if _, ok := ref["key"]; ok { + t.Fatalf("readCredentialRef key should be omitted for the API token default: %#v", ref) + } +} + +func TestTaskCreateRejectsPublicationForReadIntent(t *testing.T) { + root := newRootCmd() + root.SetArgs([]string{"task", "create", "Inspect", "--type", "agent", "--agent", "a", "--publication-credential", "repo-write"}) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), "publication flags require --workspace-intent write") { + t.Fatalf("error = %v", err) + } +} + +func TestTaskCreateRejectsIncompleteWriteWorkspace(t *testing.T) { + tests := []struct { + name string + args []string + want string + }{ + { + name: "source repository", + args: []string{"--publication-credential", "repo-write"}, + want: "--workspace-intent write requires --git-repo", + }, + { + name: "publication credential", + args: []string{"--git-repo", "https://github.com/source/repo"}, + want: "--workspace-intent write requires --publication-credential", + }, + { + name: "pull request base branch", + args: []string{ + "--git-repo", "https://github.com/source/repo", "--publication-credential", "repo-write", + "--forge-credential", "repo-forge", "--create-pr", + }, + want: "--create-pr requires --pr-base-branch", + }, + { + name: "forge credential", + args: []string{ + "--git-repo", "https://github.com/source/repo", "--publication-credential", "repo-write", + "--pr-base-branch", "main", "--create-pr", + }, + want: "--create-pr requires --forge-credential", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := newRootCmd() + args := []string{ + "task", "create", "Publish", "--type", "agent", "--agent", "a", "--workspace-intent", "write", + } + root.SetArgs(append(args, tt.args...)) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("error = %v, want %q", err, tt.want) + } + }) + } +} + +func TestTaskCreateRejectsCredentialKeyWithoutSecretName(t *testing.T) { + root := newRootCmd() + root.SetArgs([]string{ + "task", "create", "Inspect", "--type", "agent", "--agent", "a", "--read-credential-key", "custom-token", + }) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), "--read-credential-key requires --read-credential") { + t.Fatalf("error = %v", err) + } +} + +func TestSafeWorkspaceStatusDoesNotTreatWriteCredentialAsForgeCredential(t *testing.T) { + status := safeWorkspaceStatus(map[string]any{ + "spec": map[string]any{"workspace": map[string]any{ + "intent": "write", "createPR": true, + "publicationCredentialRef": map[string]any{"name": "legacy-combined-credential"}, + }}, + }) + workspace := nestedMap(status, "workspace") + if workspace["publicationCredentialConfigured"] != true || workspace["publicationWriteCredentialConfigured"] != true { + t.Fatalf("write credential summary = %#v", workspace) + } + if workspace["forgeCredentialConfigured"] != false { + t.Fatalf("write credential was silently treated as a forge credential: %#v", workspace) + } +} + +func TestTaskStatusRendersPoolDeliveryAndUnknownReplayPolicy(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{ //nolint:errcheck + "metadata": map[string]any{"name": "uncertain", "namespace": "default"}, + "spec": map[string]any{"type": "agent", "workspace": map[string]any{"intent": "write"}}, + "status": map[string]any{ + "phase": "Failed", + "execution": map[string]any{"state": "OutcomeUnknown", "outcome": "OutcomeUnknown", "runtimePoolName": "codex-write", "runtimeInstanceID": "pod:boot", "runtimeSessionGeneration": 2}, + "delivery": map[string]any{"state": "PublicationOutcomeUnknown", "branch": "orka/change"}, + }, + }) + })) + defer server.Close() + + var out bytes.Buffer + root := newRootCmd() + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"--server", server.URL, "--token", "test-token", "task", "status", "uncertain"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + for _, want := range []string{"OutcomeUnknown", "codex-write", "PublicationOutcomeUnknown", "No automatic replay"} { + if !strings.Contains(out.String(), want) { + t.Fatalf("output missing %q:\n%s", want, out.String()) + } + } +} + +func TestSafeWorkspaceStatusUsesCanonicalSpecAndDelivery(t *testing.T) { + const secretValue = "must-never-render" + status := safeWorkspaceStatus(map[string]any{ + "metadata": map[string]any{"name": "write", "namespace": "default"}, + "spec": map[string]any{"workspace": map[string]any{ + "intent": "write", "gitRepo": "https://github.com/source/repo", "createPR": true, + "readCredentialRef": map[string]any{"name": "repo-read", "key": "source-token", "value": secretValue}, + "publicationReadCredentialRef": map[string]any{"name": "repo-verify", "key": "verify-token", "value": secretValue}, + "publicationCredentialRef": map[string]any{"name": "repo-write", "key": "write-token", "value": secretValue}, + "forgeCredentialRef": map[string]any{"name": "repo-forge", "key": "forge-token", "value": secretValue}, + }}, + "status": map[string]any{"phase": "Running", "delivery": map[string]any{"state": "Publishing"}}, + }) + workspace := nestedMap(status, "workspace") + for _, field := range []string{ + "readCredentialConfigured", "publicationReadCredentialConfigured", "publicationCredentialConfigured", + "publicationWriteCredentialConfigured", "forgeCredentialConfigured", + } { + if workspace[field] != true { + t.Errorf("%s = %#v, want true", field, workspace[field]) + } + } + for _, field := range []string{ + "readCredentialRef", "publicationReadCredentialRef", "publicationCredentialRef", "forgeCredentialRef", + } { + if _, leaked := workspace[field]; leaked { + t.Errorf("safe workspace status leaked %s", field) + } + } + if strings.Contains(fmt.Sprint(status), secretValue) { + t.Fatal("safe workspace status leaked a Secret value") + } + if got := nestedString(status, "delivery", "state"); got != "Publishing" { + t.Fatalf("delivery state = %q", got) + } +} diff --git a/cmd/main.go b/cmd/main.go index 1b6233a00..68a4e0232 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,11 +11,17 @@ import ( "crypto/tls" "flag" "fmt" + "math" "os" "strconv" "strings" "time" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -29,6 +35,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" + crclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" @@ -43,13 +50,20 @@ import ( workspacev1alpha1 "github.com/orka-agents/orka/api/workspace/v1alpha1" orkaadmission "github.com/orka-agents/orka/internal/admission" "github.com/orka-agents/orka/internal/api" + "github.com/orka-agents/orka/internal/artifactcap" "github.com/orka-agents/orka/internal/contexttoken" "github.com/orka-agents/orka/internal/controller" gatewayruntime "github.com/orka-agents/orka/internal/gateway" + harnessv2 "github.com/orka-agents/orka/internal/harness/v2" + "github.com/orka-agents/orka/internal/labels" _ "github.com/orka-agents/orka/internal/llm/anthropic" _ "github.com/orka-agents/orka/internal/llm/openai" _ "github.com/orka-agents/orka/internal/metrics" "github.com/orka-agents/orka/internal/outboundaccess" + publisherservice "github.com/orka-agents/orka/internal/publisher/service" + "github.com/orka-agents/orka/internal/store" + storekube "github.com/orka-agents/orka/internal/store/kube" + "github.com/orka-agents/orka/internal/store/sqlite" "github.com/orka-agents/orka/internal/tokenexchange" "github.com/orka-agents/orka/internal/tools" @@ -59,6 +73,8 @@ import ( // +kubebuilder:scaffold:imports ) +const taskResourceKind = "Task" + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -117,6 +133,7 @@ func workspaceCleanupAPIsInstalled(mapper meta.RESTMapper) (bool, error) { // nolint:gocyclo func main() { + acpUpgradeDrainOptions := controller.DefaultACPUpgradeDrainOptions() var metricsAddr string var metricsCertPath, metricsCertName, metricsCertKey string var webhookCertPath, webhookCertName, webhookCertKey string @@ -165,6 +182,16 @@ func main() { var controllerURL string var enforceNamespaceIsolation bool var maxTasksPerNamespace int + var acpRuntimeEnabled bool + var acpIdlePoolTTL time.Duration + var acpCodexRuntimeImage string + var acpClaudeRuntimeImage string + var acpCopilotRuntimeImage string + var acpRuntimeNamespace string + var acpProviderProxyNamespace string + var acpProviderProxyBaseURL string + var acpProviderProxyPodLabels string + var acpProviderProxyTokenFile string var agentSandboxEnabled bool var agentSandboxCleanupPolicy string var oidcIssuer string @@ -241,7 +268,7 @@ func main() { flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") flag.BoolVar(&taskProvenanceAdmissionEnabled, "task-provenance-admission-enabled", - envBool("ORKA_TASK_PROVENANCE_ADMISSION_ENABLED"), + envBool("ORKA_TASK_PROVENANCE_ADMISSION_ENABLED", false), "Enable validating admission that rejects untrusted direct Task writes to Orka-managed "+ "provenance fields.") flag.StringVar(&taskProvenanceAdmissionTrustedUsers, "task-provenance-admission-trusted-users", @@ -262,13 +289,13 @@ func main() { flag.IntVar(&apiPort, "api-port", 8080, "The port the REST API server binds to.") flag.StringVar(&watchNamespace, "watch-namespace", "", "Namespace to watch for resources. Empty for all namespaces.") flag.BoolVar(&workspaceProviderAPIEnabled, "enable-workspace-provider-api", - envBool("ORKA_ENABLE_WORKSPACE_PROVIDER_API"), + envBool("ORKA_ENABLE_WORKSPACE_PROVIDER_API", false), "Enable workspace.orka.ai provider/class/pool/workspace coordination controllers.") flag.BoolVar(&workspaceClassUseAdmissionEnabled, "workspace-class-use-admission-enabled", - envBool("ORKA_WORKSPACE_CLASS_USE_ADMISSION_ENABLED"), + envBool("ORKA_WORKSPACE_CLASS_USE_ADMISSION_ENABLED", false), "Enable fail-closed Task and Tool admission checks for ExecutionWorkspaceClass use.") flag.BoolVar(&fakeWorkspaceProviderEnabled, "enable-fake-workspace-provider", - envBool("ORKA_ENABLE_FAKE_WORKSPACE_PROVIDER"), + envBool("ORKA_ENABLE_FAKE_WORKSPACE_PROVIDER", false), "Enable the development-only fake.workspace.orka.ai/v1 adapter; requires --enable-workspace-provider-api.") flag.StringVar(&aiWorkerImage, "ai-worker-image", controller.DefaultAIWorkerImage, "Container image for AI worker.") @@ -332,6 +359,26 @@ func main() { "When true, restrict users to their ServiceAccount's namespace for all operations.") flag.IntVar(&maxTasksPerNamespace, "max-tasks-per-namespace", 0, "Maximum active tasks per namespace (0 = unlimited).") + flag.BoolVar(&acpRuntimeEnabled, "acp-runtime-enabled", envBool("ORKA_ACP_RUNTIME_ENABLED", true), + "Route built-in codex/claude/copilot agent Tasks through managed orka.harness.v2 RuntimePools.") + flag.DurationVar(&acpIdlePoolTTL, "acp-idle-pool-ttl", envDurationDefault("ORKA_ACP_IDLE_POOL_TTL", controller.DefaultACPIdlePoolTTL), + "Scale an idle ACP RuntimePool to zero after this duration.") + flag.StringVar(&acpCodexRuntimeImage, "acp-codex-runtime-image", os.Getenv("ORKA_ACP_CODEX_RUNTIME_IMAGE"), + "Digest-pinned Codex ACP runtime image.") + flag.StringVar(&acpClaudeRuntimeImage, "acp-claude-runtime-image", os.Getenv("ORKA_ACP_CLAUDE_RUNTIME_IMAGE"), + "Digest-pinned Claude ACP runtime image.") + flag.StringVar(&acpCopilotRuntimeImage, "acp-copilot-runtime-image", os.Getenv("ORKA_ACP_COPILOT_RUNTIME_IMAGE"), + "Digest-pinned Copilot ACP runtime image.") + flag.StringVar(&acpRuntimeNamespace, "acp-runtime-namespace", envStringDefault("ORKA_ACP_RUNTIME_NAMESPACE", "orka-runtimes"), + "Physical namespace for managed ACP runtime Pods.") + flag.StringVar(&acpProviderProxyNamespace, "acp-provider-proxy-namespace", envStringDefault("ORKA_ACP_PROVIDER_PROXY_NAMESPACE", "vekil-system"), + "Namespace containing the approved credential-injecting provider proxy.") + flag.StringVar(&acpProviderProxyBaseURL, "acp-provider-proxy-base-url", os.Getenv("ORKA_ACP_PROVIDER_PROXY_BASE_URL"), + "Cluster-local base URL of the authenticated provider proxy boundary.") + flag.StringVar(&acpProviderProxyPodLabels, "acp-provider-proxy-pod-labels", envStringDefault("ORKA_ACP_PROVIDER_PROXY_POD_LABELS", "orka.ai/network-role=provider-auth-proxy"), + "Comma-separated exact Pod labels selected by RuntimePool provider-proxy egress policy.") + flag.StringVar(&acpProviderProxyTokenFile, "acp-provider-proxy-token-file", os.Getenv("ORKA_ACP_PROVIDER_PROXY_TOKEN_FILE"), + "Mounted file containing the authenticated provider proxy bearer token.") flag.StringVar(&executionWorkspaceDefaultProviderFlag, "execution-workspace-default-provider", executionWorkspaceDefaultProviderFlag, "Default execution workspace provider when Task execution.workspace.provider is omitted (agent-sandbox, substrate).") @@ -550,7 +597,15 @@ func main() { Development: true, } opts.BindFlags(flag.CommandLine) + acpUpgradeDrainOptions.BindFlags(flag.CommandLine) flag.Parse() + if handled, err := controller.RunACPUpgradeDrainTriggerMode(context.Background(), acpUpgradeDrainOptions); handled { + if err != nil { + fmt.Fprintln(os.Stderr, "ACP planned-upgrade drain trigger failed") + os.Exit(1) + } + return + } // Empty worker ServiceAccount flags retain the package defaults for callers that // explicitly clear a flag, matching the zero-value fallback in the controller. @@ -757,12 +812,12 @@ func main() { LeaderElectionID: "03b49a10.orka.ai", } - // Set namespace scope if specified - if watchNamespace != "" { - mgrOptions.Cache.DefaultNamespaces = map[string]cache.Config{ - watchNamespace: {}, - } - } + // Keep tenant resources namespace-scoped while allowing only RuntimePool + // child kinds to be cached from the separate ACP runtime namespace. + mgrOptions.Cache = managerCacheOptions( + watchNamespace, + acpRuntimeNamespace, + ) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions) if err != nil { @@ -819,12 +874,14 @@ func main() { Exchanger: tokenexchange.NewClient(tokenexchange.ClientOptions{}), } var brokeredTransactionExchange *worker.TransactionExchangeConfig + var brokeredTTSExchanger contexttoken.Exchanger if contextTokenTTSConfig.Enabled() { sharedTTSClient, clientErr := contexttoken.NewTTSClient(contextTokenTTSConfig) if clientErr != nil { setupLog.Error(clientErr, "unable to create brokered transaction-token exchanger") os.Exit(1) } + brokeredTTSExchanger = sharedTTSClient brokeredTransactionExchange = &worker.TransactionExchangeConfig{ TTS: contextTokenTTSConfig, Exchanger: sharedTTSClient, @@ -832,6 +889,28 @@ func main() { OutboundScope: contextTokenOutboundScope, } } + var acpMCPRegistry *tools.Registry + if acpRuntimeEnabled { + acpMCPRegistry = tools.NewRegistry() + if err := tools.RegisterBrokeredCoordinationTools(acpMCPRegistry, mgr.GetClient()); err != nil { + setupLog.Error(err, "unable to register ACP MCP broker coordination tools") + os.Exit(1) + } + if err := tools.RegisterBrokeredDelegateTaskTool( + acpMCPRegistry, + mgr.GetClient(), + tools.BrokeredDelegateTaskTransactionExchangeConfig{ + TTS: contextTokenTTSConfig, + Exchanger: brokeredTTSExchanger, + SubjectTokenType: contextTokenSubjectTokenType, + ChildScope: contextTokenChildScope, + ResolveSubjectToken: newBrokeredDelegateTaskSubjectTokenResolver(mgr.GetAPIReader(), workerenv.ServiceAccountTokenFile), + }, + ); err != nil { + setupLog.Error(err, "unable to register configured ACP delegate_task broker") + os.Exit(1) + } + } // Create SQLite store if storeBackend != "sqlite" { @@ -850,9 +929,84 @@ func main() { setupLog.Error(err, "unable to add SQLite store as runnable") os.Exit(1) } + controlNamespace, err := acpControlNamespace(acpRuntimeEnabled, currentPodNamespace()) + if err != nil { + setupLog.Error(err, "unable to configure Kubernetes ACP control store") + os.Exit(1) + } - // Create helper components + // Create helper components. Kubernetes ACP admission is feature-gated, but + // the control store, epoch manager, and cleanup recovery remain available in + // a controller Pod after admission is disabled so pre-existing durable ACP + // Sessions can still be reclaimed safely. + acpAdmissionGate := controller.NewACPAdmissionGate() sessionManager := controller.NewSessionManager(sqliteStore) + var taskCleanupControlStore store.DurableControlStore + var durableControlStore store.DurableControlStore + var controllerEpochManager *controller.ControllerEpochManager + var acpSessionContinuity *controller.ACPSessionContinuity + var kubeControlStore *storekube.Store + if controlNamespace != "" { + kubeControlStore, err = storekube.NewComposite( + mgr.GetClient(), controlNamespace, sqliteStore, storekube.WithAPIReader(mgr.GetAPIReader()), + ) + if err != nil { + setupLog.Error(err, "unable to configure Kubernetes ACP control store") + os.Exit(1) + } + controllerEpochManager = controller.NewControllerEpochManager(kubeControlStore, currentControllerHolderID()) + sessionManager.SetACPSessionCleanup(kubeControlStore, controllerEpochManager) + if err := mgr.Add(controllerEpochManager); err != nil { + setupLog.Error(err, "unable to add controller epoch manager") + os.Exit(1) + } + if err := mgr.Add(controller.NewSessionCleanupRecoveryManager(kubeControlStore, controllerEpochManager)); err != nil { + setupLog.Error(err, "unable to add Session cleanup recovery manager") + os.Exit(1) + } + } + controlStoreWiring, err := newACPControlStoreWiring(acpRuntimeEnabled, kubeControlStore) + if err != nil { + setupLog.Error(err, "unable to configure ACP control-store wiring") + os.Exit(1) + } + taskCleanupControlStore = controlStoreWiring.taskCleanup + durableControlStore = controlStoreWiring.runtime + if acpRuntimeEnabled { + acpSessionContinuity, err = controller.NewACPSessionContinuity(controller.ACPSessionContinuityConfig{ + SessionControls: durableControlStore, Transcripts: sqliteStore, Publications: durableControlStore, BranchClaims: durableControlStore, + }) + if err != nil { + setupLog.Error(err, "unable to create ACP Session continuity manager") + os.Exit(1) + } + } + + // Artifact retirement is cleanup, not admission. Keep the collector and + // Task-facing retirer active after ACP admission is disabled so durable + // Task finalization and crash-recovery sweeps can reclaim existing artifacts. + // All supported controller deployments use strategy Recreate, so an old + // artifact service cannot overlap this lock/tombstone-aware collector on the + // shared PVC during rollout. + artifactRoot := strings.TrimSpace(os.Getenv("ORKA_ACP_ARTIFACT_ROOT")) + if artifactRoot == "" { + artifactRoot = artifactcap.DefaultRoot + } + artifactRetentionWiring, err := newACPArtifactRetentionWiring(acpRuntimeEnabled, artifactRoot) + if err != nil { + setupLog.Error(err, "unable to configure ACP artifact retention") + os.Exit(1) + } + acpArtifactCollector := artifactRetentionWiring.collector + if err := mgr.Add(acpArtifactCollector); err != nil { + setupLog.Error(err, "unable to add ACP artifact retention") + os.Exit(1) + } + publisherClient, artifactCapabilitySecret, publisherWorkspaceArtifactMaxBytes, err := workspacePublisherClientFromEnv() + if err != nil { + setupLog.Error(err, "unable to configure Workspace/Publisher client") + os.Exit(1) + } sessionManager.SetGatewayEventStore(sqliteStore) maxTasksPerNamespaceValue := int32(maxTasksPerNamespace) //nolint:gosec // flag default is non-negative gatewayConfig := gatewayruntime.Config{ @@ -872,6 +1026,7 @@ func main() { setupLog.Error(err, "unable to add gateway service") os.Exit(1) } + } webhookNotifier := controller.NewWebhookNotifier() webhookNotifier.SetKubeClient(mgr.GetClient()) @@ -930,21 +1085,68 @@ func main() { agentSandboxConfig.ControllerNamespace = currentPodNamespace() } + // Keep RuntimePool finalization active after ACP admission is disabled so + // pools created by an earlier controller version can still be deleted. + runtimePoolReconciler := &controller.RuntimePoolReconciler{ + Client: mgr.GetClient(), + APIReader: mgr.GetAPIReader(), + Scheme: mgr.GetScheme(), + RuntimeNamespace: acpRuntimeNamespace, + CleanupOnly: !acpRuntimeEnabled, + } + if acpRuntimeEnabled { + providerProxyLabels, err := parseExactLabels(acpProviderProxyPodLabels) + if err != nil { + setupLog.Error(err, "unable to configure authenticated ACP provider proxy labels") + os.Exit(1) + } + runtimePoolReconciler.ControllerNamespace = controlNamespace + runtimePoolReconciler.ControllerAPIURL = jobBuilder.ControllerURL + runtimePoolReconciler.ControllerAPIPort = int32(apiPort) + runtimePoolReconciler.WorkspaceArtifactMaxBytes = publisherWorkspaceArtifactMaxBytes + runtimePoolReconciler.ProviderProxy = controller.RuntimePoolProviderProxyConfig{ + BaseURL: acpProviderProxyBaseURL, + Namespace: acpProviderProxyNamespace, + PodLabels: providerProxyLabels, + BearerTokenFile: acpProviderProxyTokenFile, + } + runtimePoolReconciler.Epochs = controllerEpochManager + runtimePoolReconciler.EnablePDB = true + runtimePoolReconciler.AllowedImages = controller.ACPRuntimeImages{ + Codex: acpCodexRuntimeImage, Claude: acpClaudeRuntimeImage, Copilot: acpCopilotRuntimeImage, + } + } + if err := runtimePoolReconciler.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "RuntimePool") + os.Exit(1) + } + // Setup Task controller with helper components if err := (&controller.TaskReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - JobBuilder: jobBuilder, - SessionManager: sessionManager, - WebhookNotifier: webhookNotifier, - KubeClient: kubeClient, - OutboundAccessResolver: outboundAccessResolver, - BrokeredTransactionExchange: brokeredTransactionExchange, - ResultStore: sqliteStore, - PlanStore: sqliteStore, - MessageStore: sqliteStore, - ArtifactStore: sqliteStore, - ExecutionEventStore: sqliteStore, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + JobBuilder: jobBuilder, + SessionManager: sessionManager, + WebhookNotifier: webhookNotifier, + KubeClient: kubeClient, + ResultStore: sqliteStore, + PlanStore: sqliteStore, + MessageStore: sqliteStore, + ArtifactStore: sqliteStore, + ExecutionEventStore: sqliteStore, + DurableControlStore: taskCleanupControlStore, + ACPArtifactRetirer: artifactRetentionWiring.taskCleanup, + ACPPublicationReclaimer: publisherClient, + ControllerEpochManager: controllerEpochManager, + ACPAdmissionGate: acpAdmissionGate, + ACPRuntimeEnabled: acpRuntimeEnabled, + ACPRuntimeImages: controller.ACPRuntimeImages{ + Codex: acpCodexRuntimeImage, Claude: acpClaudeRuntimeImage, Copilot: acpCopilotRuntimeImage, + }, + ACPRuntimeNamespace: acpRuntimeNamespace, + OutboundAccessResolver: outboundAccessResolver, + BrokeredTransactionExchange: brokeredTransactionExchange, + EnforceNamespaceIsolation: enforceNamespaceIsolation, MaxTasksPerNamespace: maxTasksPerNamespaceValue, ExecutionWorkspaceDefaultProvider: executionWorkspaceDefaultProvider, @@ -970,6 +1172,42 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Task") os.Exit(1) } + if acpRuntimeEnabled { + if err := mgr.Add(&controller.ACPDispatcher{ + Client: mgr.GetClient(), APIReader: mgr.GetAPIReader(), Store: durableControlStore, ResultStore: sqliteStore, + Epochs: controllerEpochManager, Sessions: acpSessionContinuity, + Publisher: publisherClient, ArtifactCapabilitySecret: artifactCapabilitySecret, + ArtifactReservations: acpArtifactCollector, + AdmissionGate: acpAdmissionGate, + IdlePoolTTL: acpIdlePoolTTL, + MCPRegistry: acpMCPRegistry, + }); err != nil { + setupLog.Error(err, "unable to add ACP dispatcher") + os.Exit(1) + } + if err := mgr.Add(&controller.ACPOutboxProjector{ + Client: mgr.GetClient(), Store: durableControlStore, Epochs: controllerEpochManager, WorkerID: currentControllerHolderID() + "-outbox", + }); err != nil { + setupLog.Error(err, "unable to add ACP outbox projector") + os.Exit(1) + } + if strings.TrimSpace(acpUpgradeDrainOptions.MarkerNamespace) == "" { + acpUpgradeDrainOptions.MarkerNamespace = controlNamespace + } + upgradeDrain := controller.NewACPUpgradeDrainCoordinator( + mgr.GetClient(), mgr.GetAPIReader(), controllerEpochManager, durableControlStore, + &controller.KubernetesACPUpgradeDrainBarrierObserver{Reader: mgr.GetAPIReader(), Outbox: sqliteStore}, + acpAdmissionGate, acpUpgradeDrainOptions, + ) + if err := mgr.Add(upgradeDrain); err != nil { + setupLog.Error(err, "unable to add ACP planned-upgrade drain coordinator") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("acp-upgrade-drain", upgradeDrain.ReadyzChecker()); err != nil { + setupLog.Error(err, "unable to add ACP planned-upgrade readiness check") + os.Exit(1) + } + } if err := (&controller.OutboundAccessPolicyReconciler{ Client: mgr.GetClient(), @@ -1191,6 +1429,7 @@ func main() { PlanStore: sqliteStore, MessageStore: sqliteStore, ArtifactStore: sqliteStore, + ArtifactReservations: artifactRetentionWiring.runtimeReservations, MemoryStore: sqliteStore, MemoryProposalStore: sqliteStore, SecurityStore: sqliteStore, @@ -1215,6 +1454,35 @@ func main() { MaxPrematureEndRetries: chatMaxPrematureEndRetries, }, }) + if acpRuntimeEnabled { + mcpBroker, err := controller.NewProductionACPMCPBroker(controller.ACPMCPBrokerDependencies{ + Reader: mgr.GetAPIReader(), Epochs: controllerEpochManager, ControlStore: durableControlStore, + KubeClient: kubeClient, Registry: acpMCPRegistry, + ContextFactory: func(ctx context.Context, request harnessv2.MCPBrokerCallRequest) (*tools.ToolContext, error) { + task, ok := controller.ACPMCPAuthenticatedTaskFromContext(ctx) + if !ok || task.Namespace != request.Namespace || task.UID != string(request.Metadata.TaskUID) { + return nil, fmt.Errorf("authenticated ACP MCP task context is unavailable") + } + return &tools.ToolContext{ + Client: mgr.GetClient(), KubeClient: kubeClient, Namespace: request.Namespace, + SessionID: string(request.Authorization.RuntimeSessionUID), TaskID: task.Name, + TaskUID: task.UID, ParentTaskID: task.ParentTaskID, AgentName: task.AgentName, + Tenant: request.Namespace, WatchNamespace: watchNamespace, + EnforceNamespaceIsolation: enforceNamespaceIsolation, Brokered: true, + ResultStore: sqliteStore, MessageStore: sqliteStore, SessionDeleter: sessionManager, + MemoryReader: sqliteStore, MemoryProposalWriter: sqliteStore, TranscriptSearcher: sqliteStore, + }, nil + }, + }) + if err != nil { + setupLog.Error(err, "unable to construct ACP MCP broker") + os.Exit(1) + } + if err := apiServer.RegisterACPMCPBroker(mcpBroker); err != nil { + setupLog.Error(err, "unable to register ACP MCP broker") + os.Exit(1) + } + } // Add API server as a runnable if err := mgr.Add(apiServer); err != nil { @@ -1229,10 +1497,185 @@ func main() { } } -func envBool(name string) bool { +func newBrokeredDelegateTaskSubjectTokenResolver( + reader crclient.Reader, + serviceAccountTokenFile string, +) tools.DelegateTaskSubjectTokenResolver { + return func(ctx context.Context, parentTask *corev1alpha1.Task, tokenSource string) (string, error) { + switch tokenSource { + case contexttoken.TTSTokenSourceServiceAccount: + return workerenv.ReadTokenFile(serviceAccountTokenFile, "controller service account token") + case contexttoken.TTSTokenSourceIncoming: + if reader == nil { + return "", fmt.Errorf("kubernetes reader is required for incoming brokered transaction tokens") + } + if parentTask == nil || parentTask.UID == "" { + return "", fmt.Errorf("authenticated parent Task identity is required for incoming brokered transaction tokens") + } + secretName := strings.TrimSpace(parentTask.Annotations[labels.AnnotationTransactionTokenSecret]) + if secretName == "" { + return "", fmt.Errorf("authenticated parent Task does not reference an incoming transaction-token Secret") + } + secret := &corev1.Secret{} + if err := reader.Get(ctx, crclient.ObjectKey{Name: secretName, Namespace: parentTask.Namespace}, secret); err != nil { + return "", fmt.Errorf("read authenticated parent transaction-token Secret: %w", err) + } + if !secretOwnedByTask(secret, parentTask) { + return "", fmt.Errorf("authenticated parent transaction-token Secret is not owned by the parent Task") + } + token := strings.TrimSpace(string(secret.Data["token"])) + if token == "" { + return "", fmt.Errorf("authenticated parent transaction-token Secret token is missing or empty") + } + return token, nil + case contexttoken.TTSTokenSourceNone: + return "", fmt.Errorf("context token TTS token source %q does not provide a subject token", tokenSource) + default: + return "", fmt.Errorf("unsupported context token TTS token source %q", tokenSource) + } + } +} + +func secretOwnedByTask(secret *corev1.Secret, task *corev1alpha1.Task) bool { + if secret == nil || task == nil || task.UID == "" || secret.Namespace != task.Namespace { + return false + } + for _, owner := range secret.OwnerReferences { + if owner.APIVersion == corev1alpha1.GroupVersion.String() && owner.Kind == taskResourceKind && + owner.Name == task.Name && owner.UID == task.UID { + return true + } + } + return false +} + +func workspacePublisherClientFromEnv() (*publisherservice.Client, []byte, int64, error) { + artifactSecretPath := strings.TrimSpace(os.Getenv("ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE")) + var artifactSecret []byte + if artifactSecretPath != "" { + value, err := os.ReadFile(artifactSecretPath) + if err != nil { + return nil, nil, 0, fmt.Errorf("read ACP artifact capability secret: %w", err) + } + artifactSecret = []byte(strings.TrimSpace(string(value))) + } + baseURL := strings.TrimSpace(os.Getenv("ORKA_WORKSPACE_PUBLISHER_URL")) + if baseURL == "" { + return nil, artifactSecret, artifactcap.DefaultWorkspaceArtifactMaxBytes, nil + } + bearerPath := strings.TrimSpace(os.Getenv("ORKA_WORKSPACE_PUBLISHER_CONTROLLER_TOKEN_FILE")) + capabilityPath := strings.TrimSpace(os.Getenv("ORKA_WORKSPACE_PUBLISHER_CAPABILITY_SECRET_FILE")) + if bearerPath == "" || capabilityPath == "" { + return nil, nil, 0, fmt.Errorf("Workspace/Publisher auth file paths are required") + } + bearer, err := os.ReadFile(bearerPath) + if err != nil { + return nil, nil, 0, fmt.Errorf("read Workspace/Publisher controller token: %w", err) + } + capability, err := os.ReadFile(capabilityPath) + if err != nil { + return nil, nil, 0, fmt.Errorf("read Workspace/Publisher capability secret: %w", err) + } + client, err := publisherservice.NewClient(publisherservice.ClientConfig{ + BaseURL: baseURL, BearerToken: []byte(strings.TrimSpace(string(bearer))), + CapabilitySecret: []byte(strings.TrimSpace(string(capability))), + }) + if err != nil { + return nil, nil, 0, err + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + capabilities, err := client.Capabilities(ctx) + if err != nil { + return nil, nil, 0, fmt.Errorf("read Workspace/Publisher capabilities: %w", err) + } + if capabilities.Protocol != publisherservice.ProtocolVersion { + return nil, nil, 0, fmt.Errorf("Workspace/Publisher protocol %q is incompatible", capabilities.Protocol) + } + maxArtifactBytes := capabilities.Limits.MaxWorkspaceArtifactBytes + if maxArtifactBytes <= 0 || maxArtifactBytes == math.MaxInt64 { + return nil, nil, 0, fmt.Errorf("Workspace/Publisher max workspace artifact bytes must be positive and less than %d", int64(math.MaxInt64)) + } + return client, artifactSecret, maxArtifactBytes, nil +} + +func envStringDefault(name, fallback string) string { + if value := strings.TrimSpace(os.Getenv(name)); value != "" { + return value + } + return fallback +} + +func envDurationDefault(name string, fallback time.Duration) time.Duration { value := strings.TrimSpace(os.Getenv(name)) if value == "" { - return false + return fallback + } + parsed, err := time.ParseDuration(value) + if err != nil || parsed <= 0 { + return fallback + } + return parsed +} + +func parseExactLabels(raw string) (map[string]string, error) { + result := map[string]string{} + for entry := range strings.SplitSeq(raw, ",") { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + key, value, ok := strings.Cut(entry, "=") + key, value = strings.TrimSpace(key), strings.TrimSpace(value) + if !ok || key == "" || value == "" { + return nil, fmt.Errorf("provider proxy Pod label %q must be key=value", entry) + } + if _, exists := result[key]; exists { + return nil, fmt.Errorf("provider proxy Pod label %q is duplicated", key) + } + result[key] = value + } + if len(result) == 0 { + return nil, fmt.Errorf("at least one provider proxy Pod label is required") + } + return result, nil +} + +func managerCacheOptions(watchNamespace, acpRuntimeNamespace string) cache.Options { + watchNamespace = strings.TrimSpace(watchNamespace) + if watchNamespace == "" { + return cache.Options{} + } + + options := cache.Options{ + DefaultNamespaces: map[string]cache.Config{watchNamespace: {}}, + } + runtimeNamespace := strings.TrimSpace(acpRuntimeNamespace) + if runtimeNamespace == "" { + return options + } + + runtimeChildNamespaces := map[string]cache.Config{ + watchNamespace: {}, + runtimeNamespace: {}, + } + options.ByObject = map[crclient.Object]cache.ByObject{ + &appsv1.Deployment{}: {Namespaces: runtimeChildNamespaces}, + &appsv1.ReplicaSet{}: {Namespaces: runtimeChildNamespaces}, + &corev1.Pod{}: {Namespaces: runtimeChildNamespaces}, + &corev1.Service{}: {Namespaces: runtimeChildNamespaces}, + &corev1.Secret{}: {Namespaces: runtimeChildNamespaces}, + &networkingv1.NetworkPolicy{}: {Namespaces: runtimeChildNamespaces}, + &policyv1.PodDisruptionBudget{}: {Namespaces: runtimeChildNamespaces}, + } + return options +} + +func envBool(name string, fallback bool) bool { + + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return fallback } parsed, err := strconv.ParseBool(value) if err != nil { @@ -1242,6 +1685,75 @@ func envBool(name string) bool { return parsed } +func acpControlNamespace(runtimeEnabled bool, controllerNamespace string) (string, error) { + controllerNamespace = strings.TrimSpace(controllerNamespace) + if controllerNamespace == "" { + if !runtimeEnabled { + return "", nil + } + return "", fmt.Errorf("controller namespace is unavailable") + } + return controllerNamespace, nil +} + +type acpArtifactRetentionWiring struct { + collector *artifactcap.Collector + taskCleanup artifactcap.IdentityRetirer + runtimeReservations artifactcap.CapabilityReservationRecorder +} + +func newACPArtifactRetentionWiring(runtimeEnabled bool, root string) (acpArtifactRetentionWiring, error) { + collector, err := artifactcap.NewCollector(artifactcap.CollectorConfig{Root: root}) + if err != nil { + return acpArtifactRetentionWiring{}, err + } + wiring := acpArtifactRetentionWiring{ + collector: collector, + taskCleanup: collector, + } + if runtimeEnabled { + // Capability reservations are a runtime-admission surface and are only + // exposed to the dispatcher while ACP runtime admission is enabled. + wiring.runtimeReservations = collector + } + return wiring, nil +} + +type acpControlStoreWiring struct { + taskCleanup store.DurableControlStore + runtime store.DurableControlStore +} + +func newACPControlStoreWiring(runtimeEnabled bool, kubeControlStore *storekube.Store) (acpControlStoreWiring, error) { + var wiring acpControlStoreWiring + if kubeControlStore != nil { + // Task finalization must retain the authoritative Kubernetes cleanup + // store across a true-to-false ACP feature-gate transition. + wiring.taskCleanup = kubeControlStore + } + if !runtimeEnabled { + return wiring, nil + } + if kubeControlStore == nil { + return acpControlStoreWiring{}, fmt.Errorf("kubernetes ACP control store is unavailable") + } + // Runtime admission and dispatch receive the store only while the feature + // gate is enabled; cleanup-only wiring cannot start new ACP work. + wiring.runtime = kubeControlStore + return wiring, nil +} + +func currentControllerHolderID() string { + if holder := strings.TrimSpace(os.Getenv("ORKA_CONTROLLER_HOLDER_ID")); holder != "" { + return holder + } + hostname, err := os.Hostname() + if err == nil && strings.TrimSpace(hostname) != "" { + return strings.TrimSpace(hostname) + } + return fmt.Sprintf("controller-%d", os.Getpid()) +} + func currentPodNamespace() string { if namespace := strings.TrimSpace(os.Getenv(workerenv.PodNamespace)); namespace != "" { return namespace diff --git a/cmd/main_test.go b/cmd/main_test.go index b0e4b8e03..91004e200 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -1,14 +1,471 @@ package main import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "reflect" + "slices" + "strings" "testing" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + corev1alpha1 "github.com/orka-agents/orka/api/v1alpha1" workspacev1alpha1 "github.com/orka-agents/orka/api/workspace/v1alpha1" + "github.com/orka-agents/orka/internal/artifactcap" + "github.com/orka-agents/orka/internal/contexttoken" + "github.com/orka-agents/orka/internal/labels" + publisherservice "github.com/orka-agents/orka/internal/publisher/service" + storekube "github.com/orka-agents/orka/internal/store/kube" ) +func TestBrokeredDelegateTaskSubjectTokenResolverUsesOwnedIncomingSecret(t *testing.T) { + parent := &corev1alpha1.Task{ObjectMeta: metav1.ObjectMeta{ + Name: "parent", Namespace: "team-a", UID: types.UID("parent-uid"), + Annotations: map[string]string{labels.AnnotationTransactionTokenSecret: "parent-token"}, + }} + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "parent-token", Namespace: parent.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: corev1alpha1.GroupVersion.String(), Kind: taskResourceKind, Name: parent.Name, UID: parent.UID, + }}, + }, + Data: map[string][]byte{"token": []byte(" request-scoped-token ")}, + } + reader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + resolver := newBrokeredDelegateTaskSubjectTokenResolver(reader, "") + + token, err := resolver(context.Background(), parent, contexttoken.TTSTokenSourceIncoming) + if err != nil { + t.Fatalf("resolve incoming subject token: %v", err) + } + if token != "request-scoped-token" { + t.Fatalf("resolved token = %q, want request-scoped token", token) + } +} + +func TestBrokeredDelegateTaskSubjectTokenResolverRejectsUnownedIncomingSecret(t *testing.T) { + parent := &corev1alpha1.Task{ObjectMeta: metav1.ObjectMeta{ + Name: "parent", Namespace: "team-a", UID: types.UID("parent-uid"), + Annotations: map[string]string{labels.AnnotationTransactionTokenSecret: "other-token"}, + }} + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-token", Namespace: parent.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: corev1alpha1.GroupVersion.String(), Kind: taskResourceKind, Name: "other", UID: types.UID("other-uid"), + }}, + }, + Data: map[string][]byte{"token": []byte("must-not-be-used")}, + } + reader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + resolver := newBrokeredDelegateTaskSubjectTokenResolver(reader, "") + + _, err := resolver(context.Background(), parent, contexttoken.TTSTokenSourceIncoming) + if err == nil || !strings.Contains(err.Error(), "not owned by the parent Task") { + t.Fatalf("resolve incoming subject token error = %v, want owner rejection", err) + } +} + +func TestBrokeredDelegateTaskSubjectTokenResolverReadsControllerServiceAccountPerRequest(t *testing.T) { + path := filepath.Join(t.TempDir(), "service-account-token") + if err := os.WriteFile(path, []byte(" controller-service-account-token "), 0o600); err != nil { + t.Fatal(err) + } + resolver := newBrokeredDelegateTaskSubjectTokenResolver(nil, path) + + token, err := resolver(context.Background(), &corev1alpha1.Task{}, contexttoken.TTSTokenSourceServiceAccount) + if err != nil { + t.Fatalf("resolve service account subject token: %v", err) + } + if token != "controller-service-account-token" { + t.Fatalf("resolved service account token = %q", token) + } +} + +func TestWorkspacePublisherClientFromEnvUsesBoundedFallbackWithoutPublisher(t *testing.T) { + t.Setenv("ORKA_WORKSPACE_PUBLISHER_URL", "") + t.Setenv("ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE", "") + + publisherClient, artifactSecret, gotLimit, err := workspacePublisherClientFromEnv() + if err != nil { + t.Fatal(err) + } + if publisherClient != nil { + t.Fatal("Workspace/Publisher client is non-nil") + } + if len(artifactSecret) != 0 { + t.Fatalf("artifact capability secret length = %d, want 0", len(artifactSecret)) + } + if gotLimit != artifactcap.DefaultWorkspaceArtifactMaxBytes { + t.Fatalf("workspace artifact fallback = %d, want %d", gotLimit, artifactcap.DefaultWorkspaceArtifactMaxBytes) + } +} + +func TestWorkspacePublisherClientFromEnvNegotiatesWorkspaceArtifactLimit(t *testing.T) { + const wantLimit = int64(192 << 20) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path != publisherservice.CapabilitiesPath { + http.NotFound(writer, request) + return + } + writer.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(writer).Encode(publisherservice.CapabilitiesResponse{ + Protocol: publisherservice.ProtocolVersion, + Limits: publisherservice.CapabilityLimits{ + MaxWorkspaceArtifactBytes: wantLimit, + }, + }); err != nil { + t.Errorf("encode capabilities: %v", err) + } + })) + defer server.Close() + + dir := t.TempDir() + bearerPath := filepath.Join(dir, "controller-token") + capabilityPath := filepath.Join(dir, "capability-secret") + if err := os.WriteFile(bearerPath, []byte("controller-token-0123456789abcdef"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(capabilityPath, []byte("capability-secret-0123456789abcdef"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("ORKA_WORKSPACE_PUBLISHER_URL", server.URL) + t.Setenv("ORKA_WORKSPACE_PUBLISHER_CONTROLLER_TOKEN_FILE", bearerPath) + t.Setenv("ORKA_WORKSPACE_PUBLISHER_CAPABILITY_SECRET_FILE", capabilityPath) + t.Setenv("ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE", "") + + publisherClient, artifactSecret, gotLimit, err := workspacePublisherClientFromEnv() + if err != nil { + t.Fatal(err) + } + if publisherClient == nil { + t.Fatal("Workspace/Publisher client is nil") + } + if len(artifactSecret) != 0 { + t.Fatalf("artifact capability secret length = %d, want 0", len(artifactSecret)) + } + if gotLimit != wantLimit { + t.Fatalf("workspace artifact limit = %d, want %d", gotLimit, wantLimit) + } +} + +func TestWorkspacePublisherClientFromEnvRejectsInvalidArtifactCapability(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + writer.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(writer).Encode(publisherservice.CapabilitiesResponse{ + Protocol: publisherservice.ProtocolVersion, + }) + })) + defer server.Close() + + dir := t.TempDir() + bearerPath := filepath.Join(dir, "controller-token") + capabilityPath := filepath.Join(dir, "capability-secret") + if err := os.WriteFile(bearerPath, []byte("controller-token-0123456789abcdef"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(capabilityPath, []byte("capability-secret-0123456789abcdef"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("ORKA_WORKSPACE_PUBLISHER_URL", server.URL) + t.Setenv("ORKA_WORKSPACE_PUBLISHER_CONTROLLER_TOKEN_FILE", bearerPath) + t.Setenv("ORKA_WORKSPACE_PUBLISHER_CAPABILITY_SECRET_FILE", capabilityPath) + t.Setenv("ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE", "") + + if _, _, _, err := workspacePublisherClientFromEnv(); err == nil { + t.Fatal("workspacePublisherClientFromEnv() error = nil, want invalid Publisher limit") + } +} + +func TestACPControlNamespace(t *testing.T) { + tests := []struct { + name string + runtimeEnabled bool + controllerNamespace string + want string + wantErr bool + }{ + { + name: "disabled runtime does not require controller namespace", + }, + { + name: "disabled runtime keeps discovered controller namespace for cleanup", + controllerNamespace: "orka-system", + want: "orka-system", + }, + { + name: "enabled runtime fails closed without controller namespace", + runtimeEnabled: true, + wantErr: true, + }, + { + name: "enabled runtime rejects blank controller namespace", + runtimeEnabled: true, + controllerNamespace: " ", + wantErr: true, + }, + { + name: "enabled runtime uses controller namespace", + runtimeEnabled: true, + controllerNamespace: " orka-system ", + want: "orka-system", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := acpControlNamespace(tt.runtimeEnabled, tt.controllerNamespace) + if (err != nil) != tt.wantErr { + t.Fatalf("acpControlNamespace() error = %v, wantErr %t", err, tt.wantErr) + } + if got != tt.want { + t.Fatalf("acpControlNamespace() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestACPArtifactRetentionWiring(t *testing.T) { + tests := []struct { + name string + runtimeEnabled bool + wantRuntimeReservations bool + }{ + { + name: "disabled runtime retains cleanup collector only", + }, + { + name: "enabled runtime exposes reservation recorder", + runtimeEnabled: true, + wantRuntimeReservations: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wiring, err := newACPArtifactRetentionWiring( + tt.runtimeEnabled, + filepath.Join(t.TempDir(), "artifacts"), + ) + if err != nil { + t.Fatalf("newACPArtifactRetentionWiring() error = %v", err) + } + if wiring.collector == nil { + t.Fatal("collector is nil") + } + if wiring.taskCleanup != wiring.collector { + t.Fatal("Task cleanup retirer does not preserve the collector") + } + if got := wiring.runtimeReservations != nil; got != tt.wantRuntimeReservations { + t.Fatalf("runtime reservation recorder present = %t, want %t", got, tt.wantRuntimeReservations) + } + if wiring.runtimeReservations != nil && wiring.runtimeReservations != wiring.collector { + t.Fatal("runtime reservation recorder does not preserve the collector") + } + if !wiring.collector.NeedLeaderElection() { + t.Fatal("collector must remain a leader-elected cleanup runnable") + } + }) + } +} + +func TestACPArtifactRetentionWiringFailsClosedInCleanupOnlyMode(t *testing.T) { + if _, err := newACPArtifactRetentionWiring(false, "relative/artifacts"); err == nil { + t.Fatal("newACPArtifactRetentionWiring() error = nil, want unsafe-root error") + } +} + +func TestACPControlStoreWiring(t *testing.T) { + tests := []struct { + name string + runtimeEnabled bool + withStore bool + wantTaskCleanup bool + wantRuntime bool + wantErr bool + }{ + { + name: "disabled runtime without controller namespace has no control store", + }, + { + name: "disabled runtime retains Task cleanup store only", + withStore: true, + wantTaskCleanup: true, + }, + { + name: "enabled runtime shares store with Task cleanup", + runtimeEnabled: true, + withStore: true, + wantTaskCleanup: true, + wantRuntime: true, + }, + { + name: "enabled runtime fails closed without control store", + runtimeEnabled: true, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var kubeControlStore *storekube.Store + if tt.withStore { + kubeControlStore = &storekube.Store{} + } + + wiring, err := newACPControlStoreWiring(tt.runtimeEnabled, kubeControlStore) + if (err != nil) != tt.wantErr { + t.Fatalf("newACPControlStoreWiring() error = %v, wantErr %t", err, tt.wantErr) + } + if tt.wantErr { + return + } + if got := wiring.taskCleanup; (got != nil) != tt.wantTaskCleanup { + t.Fatalf("task cleanup store present = %t, want %t", got != nil, tt.wantTaskCleanup) + } else if got != nil && got != kubeControlStore { + t.Fatal("task cleanup store does not preserve the Kubernetes store") + } + if got := wiring.runtime; (got != nil) != tt.wantRuntime { + t.Fatalf("runtime store present = %t, want %t", got != nil, tt.wantRuntime) + } else if got != nil && got != kubeControlStore { + t.Fatal("runtime store does not preserve the Kubernetes store") + } + }) + } +} + +func TestManagerCacheOptions(t *testing.T) { + childTypes := []client.Object{ + &appsv1.Deployment{}, + &appsv1.ReplicaSet{}, + &corev1.Pod{}, + &corev1.Service{}, + &corev1.Secret{}, + &networkingv1.NetworkPolicy{}, + &policyv1.PodDisruptionBudget{}, + } + tests := []struct { + name string + watchNamespace string + runtimeNamespace string + wantDefault []string + wantRuntimeChild []string + wantChildOverrides bool + }{ + { + name: "cluster-wide watch is unrestricted", + runtimeNamespace: "orka-runtimes", + }, + { + name: "tenant defaults and distinct runtime child namespace", + watchNamespace: "tenant-a", + runtimeNamespace: "orka-runtimes", + wantDefault: []string{"tenant-a"}, + wantRuntimeChild: []string{"orka-runtimes", "tenant-a"}, + wantChildOverrides: true, + }, + { + name: "identical tenant and runtime namespaces are deduplicated", + watchNamespace: "tenant-a", + runtimeNamespace: "tenant-a", + wantDefault: []string{"tenant-a"}, + wantRuntimeChild: []string{"tenant-a"}, + wantChildOverrides: true, + }, + { + name: "runtime cleanup watches remain active when admission is disabled", + watchNamespace: "tenant-a", + runtimeNamespace: "orka-runtimes", + wantDefault: []string{"tenant-a"}, + wantRuntimeChild: []string{"orka-runtimes", "tenant-a"}, + wantChildOverrides: true, + }, + { + name: "blank runtime namespace keeps tenant defaults", + watchNamespace: "tenant-a", + runtimeNamespace: " ", + wantDefault: []string{"tenant-a"}, + wantRuntimeChild: []string{"tenant-a"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := managerCacheOptions(tt.watchNamespace, tt.runtimeNamespace) + assertCacheNamespaces(t, options.DefaultNamespaces, tt.wantDefault) + + for _, object := range []client.Object{ + &corev1alpha1.Task{}, + &corev1alpha1.Agent{}, + &corev1.ConfigMap{}, + } { + if _, ok := cacheByObjectForType(options, object); ok { + t.Fatalf("default-cached object %T unexpectedly has a ByObject override", object) + } + assertCacheNamespaces(t, effectiveCacheNamespaces(options, object), tt.wantDefault) + } + + if got := len(options.ByObject); tt.wantChildOverrides && got != len(childTypes) { + t.Fatalf("ByObject override count = %d, want %d", got, len(childTypes)) + } else if !tt.wantChildOverrides && got != 0 { + t.Fatalf("ByObject override count = %d, want 0", got) + } + for _, object := range childTypes { + _, overridden := cacheByObjectForType(options, object) + if overridden != tt.wantChildOverrides { + t.Fatalf("ByObject override for %T = %t, want %t", object, overridden, tt.wantChildOverrides) + } + assertCacheNamespaces(t, effectiveCacheNamespaces(options, object), tt.wantRuntimeChild) + } + }) + } +} + +func effectiveCacheNamespaces(options cache.Options, object client.Object) map[string]cache.Config { + if byObject, ok := cacheByObjectForType(options, object); ok { + return byObject.Namespaces + } + return options.DefaultNamespaces +} + +func cacheByObjectForType(options cache.Options, object client.Object) (cache.ByObject, bool) { + objectType := reflect.TypeOf(object) + for candidate, byObject := range options.ByObject { + if reflect.TypeOf(candidate) == objectType { + return byObject, true + } + } + return cache.ByObject{}, false +} + +func assertCacheNamespaces(t *testing.T, namespaces map[string]cache.Config, want []string) { + t.Helper() + got := make([]string, 0, len(namespaces)) + for namespace := range namespaces { + got = append(got, namespace) + } + slices.Sort(got) + if !slices.Equal(got, want) { + t.Fatalf("cache namespaces = %v, want %v", got, want) + } +} + func TestWorkspaceCleanupAPIsInstalled(t *testing.T) { mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{workspacev1alpha1.GroupVersion}) mapper.Add( diff --git a/cmd/orka-acp-exec-helper/main.go b/cmd/orka-acp-exec-helper/main.go new file mode 100644 index 000000000..2a2044a4b --- /dev/null +++ b/cmd/orka-acp-exec-helper/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "github.com/orka-agents/orka/internal/acp" +) + +func main() { + if err := acp.RunExecHelper(os.Args[1:], os.Environ()); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "ACP exec helper failed: %v\n", err) + os.Exit(126) + } +} diff --git a/cmd/orka-acp-runtime/main.go b/cmd/orka-acp-runtime/main.go new file mode 100644 index 000000000..f756189bb --- /dev/null +++ b/cmd/orka-acp-runtime/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "errors" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/orka-agents/orka/internal/acp" + "github.com/orka-agents/orka/workers/acp/supervisor" +) + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) + slog.SetDefault(logger) + if _, err := acp.HardenSupervisorProcess(); err != nil { + logger.Error("failed to harden ACP supervisor", "error", err) + os.Exit(1) + } + cfg, err := supervisor.LoadConfigFromEnv() + if err != nil { + logger.Error("invalid ACP supervisor configuration", "error", err) + os.Exit(1) + } + runtimeServer, err := supervisor.New(cfg) + cfg.ProviderProxy.UpstreamBearerToken = "" + if err != nil { + logger.Error("create ACP supervisor", "error", err) + os.Exit(1) + } + httpServer := &http.Server{ + Addr: cfg.ListenAddress, + Handler: runtimeServer.Handler(), + ReadHeaderTimeout: 10 * time.Second, + IdleTimeout: 2 * time.Minute, + MaxHeaderBytes: 32 << 10, + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + defer stop() + shutdownResult := make(chan error, 1) + go func() { + <-ctx.Done() + runtimeServer.BeginDrain("process_shutdown") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + shutdownErr := httpServer.Shutdown(shutdownCtx) + if shutdownErr != nil { + shutdownErr = errors.Join(shutdownErr, httpServer.Close()) + } + shutdownResult <- shutdownErr + }() + + logger.Info( + "ACP supervisor listening", "address", cfg.ListenAddress, "provider", cfg.Provider.Kind, + "runtimeInstanceID", cfg.Fence.RuntimeInstanceID, + ) + serveErr := httpServer.ListenAndServe() + if errors.Is(serveErr, http.ErrServerClosed) { + serveErr = nil + } + var shutdownErr error + if ctx.Err() != nil { + shutdownErr = <-shutdownResult + } else if serveErr != nil { + runtimeServer.BeginDrain("http_serve_failed") + shutdownErr = httpServer.Close() + } + cleanupCtx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + cleanupErr := runtimeServer.Close(cleanupCtx) + if err := errors.Join(serveErr, shutdownErr, cleanupErr); err != nil { + logger.Error("ACP supervisor stopped with incomplete cleanup", "error", err) + os.Exit(1) + } +} diff --git a/cmd/orka-agent-harness-wrapper/.gitignore b/cmd/orka-agent-harness-wrapper/.gitignore deleted file mode 100644 index 5ec983e30..000000000 --- a/cmd/orka-agent-harness-wrapper/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Bundled Copilot CLI artifacts generated by github.com/github/copilot-sdk/go/cmd/bundler. -*.zst -*.license -zcopilot_*.go diff --git a/cmd/orka-agent-harness-wrapper/main.go b/cmd/orka-agent-harness-wrapper/main.go deleted file mode 100644 index b676d775c..000000000 --- a/cmd/orka-agent-harness-wrapper/main.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "github.com/orka-agents/orka/internal/tracing" - "github.com/orka-agents/orka/internal/workerenv" - "github.com/orka-agents/orka/workers/harness/cliwrapper" -) - -type repeatedString []string - -func (r *repeatedString) String() string { return strings.Join(*r, ",") } -func (r *repeatedString) Set(value string) error { - *r = append(*r, value) - return nil -} - -func main() { - if err := run(os.Args[1:]); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run(args []string) error { - if len(args) > 0 && args[0] == "copilot-turn" { - return cliwrapper.RunCopilotTurnCLI(context.Background(), os.Stdin, os.Stdout) - } - cfg, err := cliwrapper.LoadConfigFromEnvUnvalidated() - if err != nil { - return err - } - _ = os.Unsetenv(cliwrapper.EnvAuthValue) - authValueFromEnv := cfg.AuthValue - cfg.AuthValue = "" - var extraArgs repeatedString - var extraEnv repeatedString - fs := flag.NewFlagSet("orka-agent-harness-wrapper", flag.ContinueOnError) - fs.SetOutput(os.Stderr) - fs.StringVar(&cfg.ListenAddr, "listen-addr", cfg.ListenAddr, "HTTP listen address") - fs.StringVar(&cfg.Runtime, "runtime", cfg.Runtime, "runtime adapter: generic, codex, claude, copilot, opencode, multi") - fs.StringVar(&cfg.WorkDir, "workdir", cfg.WorkDir, "default command working directory") - fs.StringVar(&cfg.Generic.Command, "command", cfg.Generic.Command, "generic adapter command path") - fs.Var(&extraArgs, "arg", "generic adapter command argument (repeatable)") - fs.Var(&extraEnv, "env", "generic adapter command environment entry KEY=VALUE (repeatable)") - fs.StringVar(&cfg.Generic.PromptMode, "prompt-mode", cfg.Generic.PromptMode, "generic prompt mode: stdin, env, file") - fs.StringVar(&cfg.Generic.PromptEnv, "prompt-env", cfg.Generic.PromptEnv, "env var used for prompt env/file modes") - fs.StringVar(&cfg.Generic.PromptFile, "prompt-file", cfg.Generic.PromptFile, "prompt file path for prompt-mode=file") - fs.StringVar(&cfg.Generic.ResultMode, "result-mode", cfg.Generic.ResultMode, "generic result mode: stdout, file") - fs.StringVar(&cfg.Generic.ResultFile, "result-file", cfg.Generic.ResultFile, "result file path for result-mode=file") - fs.Int64Var(&cfg.StdoutLimitBytes, "stdout-limit-bytes", cfg.StdoutLimitBytes, "stdout capture limit") - fs.Int64Var(&cfg.StderrLimitBytes, "stderr-limit-bytes", cfg.StderrLimitBytes, "stderr capture limit") - fs.DurationVar(&cfg.CancelGrace, "cancel-grace-period", cfg.CancelGrace, "SIGTERM to SIGKILL grace period") - fs.DurationVar(&cfg.TurnRetention, "turn-retention", cfg.TurnRetention, "completed turn in-memory retention TTL") - fs.StringVar(&cfg.Copilot.Path, "copilot-cli-path", cfg.Copilot.Path, "Copilot CLI path for the copilot adapter") - fs.StringVar( - &cfg.Copilot.HelperPath, - "copilot-helper-path", - cfg.Copilot.HelperPath, - "helper executable path for the copilot adapter", - ) - fs.StringVar(&cfg.AuthValue, "bearer-token", cfg.AuthValue, "required bearer token for turn/event/cancel endpoints") - fs.BoolVar( - &cfg.AllowUnauthenticated, - "allow-unauthenticated", - cfg.AllowUnauthenticated, - "allow unauthenticated turn/event/cancel requests (local tests only)", - ) - if err := fs.Parse(args); err != nil { - return err - } - if len(extraArgs) > 0 { - cfg.Generic.Args = append(cfg.Generic.Args, extraArgs...) - } - if cfg.AuthValue == "" { - cfg.AuthValue = authValueFromEnv - } - if len(extraEnv) > 0 { - cfg.Generic.Env = append(cfg.Generic.Env, extraEnv...) - cfg.CommandEnv = append(cfg.CommandEnv, extraEnv...) - } - if cfg.WorkDir != "" { - cfg.Generic.WorkDir = cfg.WorkDir - cfg.Codex.WorkDir = cfg.WorkDir - cfg.Claude.WorkDir = cfg.WorkDir - cfg.Copilot.WorkDir = cfg.WorkDir - cfg.Opencode.WorkDir = cfg.WorkDir - } - telemetryEnabled := workerenv.IsTrue(os.Getenv(workerenv.EnableTelemetry)) - tracingShutdown, err := tracing.Init("orka-agent-harness-wrapper", telemetryEnabled) - if err != nil { - return fmt.Errorf("failed to initialize telemetry: %w", err) - } - defer func() { - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer shutdownCancel() - if shutdownErr := tracingShutdown(shutdownCtx); shutdownErr != nil { - fmt.Fprintf(os.Stderr, "warning: failed to shutdown telemetry: %v\n", shutdownErr) - } - }() - - adapter, err := cliwrapper.NewRuntimeAdapter(cfg) - if err != nil { - return err - } - server, err := cliwrapper.NewServer(cfg, adapter) - if err != nil { - return err - } - httpServer := &http.Server{Addr: cfg.ListenAddr, Handler: server.Handler()} - errCh := make(chan error, 1) - go func() { - fmt.Fprintf(os.Stderr, "orka agent harness wrapper listening on %s (runtime=%s)\n", cfg.ListenAddr, adapter.Name()) - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errCh <- err - return - } - errCh <- nil - }() - - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - select { - case <-ctx.Done(): - shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.CancelGrace) - defer cancel() - return httpServer.Shutdown(shutdownCtx) - case err := <-errCh: - return err - } -} diff --git a/cmd/orka-image-ref-validator/main.go b/cmd/orka-image-ref-validator/main.go new file mode 100644 index 000000000..5de986c4f --- /dev/null +++ b/cmd/orka-image-ref-validator/main.go @@ -0,0 +1,83 @@ +package main + +import ( + _ "crypto/sha256" + "fmt" + "net" + "os" + "strconv" + "strings" + + distributionref "github.com/distribution/reference" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "at least one container image reference is required") + os.Exit(2) + } + for _, value := range os.Args[1:] { + if err := validateImageReference(value); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + } +} + +func validateImageReference(value string) error { + if strings.ContainsAny(value, "\r\n") { + return fmt.Errorf("container image reference contains a newline") + } + named, err := distributionref.ParseNormalizedNamed(value) + if err != nil { + return fmt.Errorf("invalid container image reference %q: %w", value, err) + } + if err := validateRegistryTransport(distributionref.Domain(named)); err != nil { + return fmt.Errorf("invalid container image reference %q: %w", value, err) + } + digested, ok := named.(distributionref.Digested) + if !ok { + return fmt.Errorf("container image reference %q is not digest pinned", value) + } + digest := digested.Digest().String() + if len(digest) != len("sha256:")+64 || !strings.HasPrefix(digest, "sha256:") { + return fmt.Errorf("container image reference %q must use a sha256 digest", value) + } + return nil +} + +func validateRegistryTransport(domain string) error { + port := "" + if after, ok := strings.CutPrefix(domain, "["); ok { + var host string + if strings.Contains(domain, "]:") { + var err error + host, port, err = net.SplitHostPort(domain) + if err != nil { + return fmt.Errorf("invalid bracketed registry host: %w", err) + } + } else { + if !strings.HasSuffix(domain, "]") { + return fmt.Errorf("invalid bracketed registry host") + } + host = strings.TrimSuffix(after, "]") + } + ip := net.ParseIP(host) + if ip == nil || !strings.Contains(host, ":") { + return fmt.Errorf("bracketed registry host is not a valid IPv6 address") + } + } else if strings.Contains(domain, ":") { + var err error + _, port, err = net.SplitHostPort(domain) + if err != nil { + return fmt.Errorf("invalid registry host or port: %w", err) + } + } + if port != "" { + value, err := strconv.ParseUint(port, 10, 16) + if err != nil || value == 0 { + return fmt.Errorf("registry port must be between 1 and 65535") + } + } + return nil +} diff --git a/cmd/orka-image-ref-validator/main_test.go b/cmd/orka-image-ref-validator/main_test.go new file mode 100644 index 000000000..cc5257755 --- /dev/null +++ b/cmd/orka-image-ref-validator/main_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "strings" + "testing" +) + +func TestValidateImageReference(t *testing.T) { + digest := "sha256:" + strings.Repeat("a", 64) + for _, value := range []string{ + "docker.io/example/acp@" + digest, + "registry--prod.example.com:5000/team/acp:release@" + digest, + "[2001:db8::1]:5000/team/acp@" + digest, + "acp@" + digest, + } { + if err := validateImageReference(value); err != nil { + t.Errorf("validateImageReference(%q) error = %v", value, err) + } + } + + longPath := "docker.io/" + strings.Repeat("a", 256) + "@" + digest + for _, value := range []string{ + "not-digest-pinned", + "https://registry.example.com/team/acp@" + digest, + "registry.example.com:notaport/team/acp@" + digest, + "registry.example.com:70000/team/acp@" + digest, + "[127.0.0.1]/team/acp@" + digest, + "[:::]/team/acp@" + digest, + "docker.io/team/@" + digest, + "docker.io/example/acp\n#@" + digest, + longPath, + } { + if err := validateImageReference(value); err == nil { + t.Errorf("validateImageReference(%q) error = nil, want rejection", value) + } + } +} diff --git a/cmd/orka-provider-auth-proxy/credentials.go b/cmd/orka-provider-auth-proxy/credentials.go new file mode 100644 index 000000000..2e9b81e55 --- /dev/null +++ b/cmd/orka-provider-auth-proxy/credentials.go @@ -0,0 +1,436 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/subtle" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "time" +) + +const ( + defaultTokenReloadInterval = 5 * time.Second + defaultPreviousTokenOverlap = 10 * time.Minute + maxPreviousTokenOverlap = 24 * time.Hour + tokenFileReadAttempts = 3 + tokenFileReadRetryDelay = 2 * time.Millisecond + maxBearerTokenBytes = 4096 + maxTokenDeadlineBytes = 128 +) + +var errTokenReload = errors.New("provider auth token reload failed") + +type bearerTokenSnapshot struct { + currentDigest [sha256.Size]byte + previousDigest [sha256.Size]byte + previousValidUntil time.Time + hasPrevious bool + ready bool +} + +type bearerTokenStore struct { + now func() time.Time + snapshot atomic.Pointer[bearerTokenSnapshot] +} + +func newBearerTokenStore(now func() time.Time) *bearerTokenStore { + if now == nil { + now = time.Now + } + store := &bearerTokenStore{now: now} + store.disable() + return store +} + +func newStaticBearerTokenStore(token []byte) (*bearerTokenStore, error) { + if err := validateBearerToken(token); err != nil { + return nil, err + } + store := newBearerTokenStore(time.Now) + store.activate(token, nil, time.Time{}) + return store, nil +} + +func (s *bearerTokenStore) activate(current, previous []byte, previousValidUntil time.Time) { + next := &bearerTokenSnapshot{ + currentDigest: sha256.Sum256(current), + ready: true, + } + if len(previous) != 0 { + next.previousDigest = sha256.Sum256(previous) + next.previousValidUntil = previousValidUntil + next.hasPrevious = true + } + s.snapshot.Store(next) +} + +func (s *bearerTokenStore) disable() { + s.snapshot.Store(&bearerTokenSnapshot{}) +} + +func (s *bearerTokenStore) isReady() bool { + return s.snapshot.Load().ready +} + +func (s *bearerTokenStore) authorized(values []string) bool { + if len(values) != 1 { + return false + } + scheme, credential, ok := strings.Cut(values[0], " ") + if !ok || !strings.EqualFold(scheme, "Bearer") || credential == "" || strings.ContainsAny(credential, " \t\r\n") { + return false + } + active := s.snapshot.Load() + if !active.ready { + return false + } + provided := sha256.Sum256([]byte(credential)) + currentMatch := subtle.ConstantTimeCompare(provided[:], active.currentDigest[:]) + previousMatch := 0 + if active.hasPrevious && s.now().Before(active.previousValidUntil) { + previousMatch = subtle.ConstantTimeCompare(provided[:], active.previousDigest[:]) + } + return currentMatch|previousMatch == 1 +} + +type tokenFileReloaderConfig struct { + CurrentTokenFile string + PreviousTokenFile string + PreviousTokenValidUntilFile string + ReloadInterval time.Duration + PreviousTokenOverlap time.Duration +} + +type tokenFileReloader struct { + config tokenFileReloaderConfig + store *bearerTokenStore + now func() time.Time + mu sync.Mutex +} + +func newTokenFileReloader(config tokenFileReloaderConfig, store *bearerTokenStore) (*tokenFileReloader, error) { + config.CurrentTokenFile = strings.TrimSpace(config.CurrentTokenFile) + config.PreviousTokenFile = strings.TrimSpace(config.PreviousTokenFile) + config.PreviousTokenValidUntilFile = strings.TrimSpace(config.PreviousTokenValidUntilFile) + if config.CurrentTokenFile == "" { + return nil, fmt.Errorf("current provider auth token file is required") + } + if (config.PreviousTokenFile == "") != (config.PreviousTokenValidUntilFile == "") { + return nil, fmt.Errorf("previous provider auth token and validity files must be configured together") + } + if pathsOverlap(config.CurrentTokenFile, config.PreviousTokenFile, config.PreviousTokenValidUntilFile) { + return nil, fmt.Errorf("provider auth token file paths must differ") + } + if config.ReloadInterval <= 0 { + return nil, fmt.Errorf("provider auth token reload interval must be positive") + } + if config.PreviousTokenOverlap <= 0 || config.PreviousTokenOverlap > maxPreviousTokenOverlap { + return nil, fmt.Errorf("provider auth previous token overlap must be positive and at most %s", maxPreviousTokenOverlap) + } + if store == nil { + return nil, fmt.Errorf("provider auth token store is required") + } + return &tokenFileReloader{ + config: config, + store: store, + now: store.now, + }, nil +} + +func pathsOverlap(paths ...string) bool { + seen := make(map[string]struct{}, len(paths)) + for _, path := range paths { + if path == "" { + continue + } + if _, ok := seen[path]; ok { + return true + } + seen[path] = struct{}{} + } + return false +} + +func (r *tokenFileReloader) reload() error { + r.mu.Lock() + defer r.mu.Unlock() + + current, previous, validUntilFile, err := readStableTokenFiles( + r.config.CurrentTokenFile, + r.config.PreviousTokenFile, + r.config.PreviousTokenValidUntilFile, + ) + if err != nil { + r.store.disable() + return errTokenReload + } + defer clear(current.contents) + defer clear(previous.contents) + defer clear(validUntilFile.contents) + normalizeMountedToken(¤t) + normalizeMountedToken(&previous) + + previousValidUntil, err := r.validateTokenFiles(current, previous, validUntilFile) + if err != nil { + r.store.disable() + return errTokenReload + } + r.store.activate(current.contents, previous.contents, previousValidUntil) + return nil +} + +func normalizeMountedToken(snapshot *tokenFileSnapshot) { + if snapshot == nil || len(snapshot.contents) == 0 { + return + } + trimmed := bytes.TrimSpace(snapshot.contents) + copy(snapshot.contents, trimmed) + clear(snapshot.contents[len(trimmed):]) + snapshot.contents = snapshot.contents[:len(trimmed)] +} + +func (r *tokenFileReloader) validateTokenFiles( + current tokenFileSnapshot, + previous tokenFileSnapshot, + validUntilFile tokenFileSnapshot, +) (time.Time, error) { + if err := validateBearerToken(current.contents); err != nil { + return time.Time{}, errTokenReload + } + if previous.present != validUntilFile.present { + return time.Time{}, errTokenReload + } + if !previous.present { + return time.Time{}, nil + } + if err := validateBearerToken(previous.contents); err != nil { + return time.Time{}, errTokenReload + } + currentDigest := sha256.Sum256(current.contents) + previousDigest := sha256.Sum256(previous.contents) + if subtle.ConstantTimeCompare(currentDigest[:], previousDigest[:]) == 1 { + return time.Time{}, errTokenReload + } + validUntil, err := parseTokenDeadline(validUntilFile.contents) + if err != nil || validUntil.After(r.now().Add(r.config.PreviousTokenOverlap)) { + return time.Time{}, errTokenReload + } + return validUntil, nil +} + +func (r *tokenFileReloader) run(ctx context.Context, logMessage func(string)) { + ticker := time.NewTicker(r.config.ReloadInterval) + defer ticker.Stop() + failed := false + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := r.reload(); err != nil { + if !failed && logMessage != nil { + logMessage("provider auth proxy token reload failed; authentication is disabled until a valid reload") + } + failed = true + continue + } + if failed && logMessage != nil { + logMessage("provider auth proxy token reload recovered") + } + failed = false + } + } +} + +func parseTokenDeadline(contents []byte) (time.Time, error) { + if len(contents) == 0 || len(contents) > maxTokenDeadlineBytes { + return time.Time{}, errTokenReload + } + value := strings.TrimSpace(string(contents)) + if value == "" || strings.ContainsAny(value, "\r\n\t ") { + return time.Time{}, errTokenReload + } + parsed, err := time.Parse(time.RFC3339Nano, value) + if err != nil { + return time.Time{}, errTokenReload + } + return parsed, nil +} + +type tokenFileSnapshot struct { + contents []byte + info os.FileInfo + present bool +} + +func readStableTokenFiles( + currentPath string, + previousPath string, + validUntilPath string, +) (tokenFileSnapshot, tokenFileSnapshot, tokenFileSnapshot, error) { + return readStableTokenFilesWithWait(currentPath, previousPath, validUntilPath, waitForNextTokenFileRead) +} + +func readStableTokenFilesWithWait( + currentPath string, + previousPath string, + validUntilPath string, + wait func(int), +) (tokenFileSnapshot, tokenFileSnapshot, tokenFileSnapshot, error) { + configuredPaths := [3]string{currentPath, previousPath, validUntilPath} + for attempt := range tokenFileReadAttempts { + readPaths, generation, projected, err := credentialReadPaths(configuredPaths) + if err != nil { + wait(attempt) + continue + } + current, err := readTokenFile(readPaths[0], false, maxBearerTokenBytes) + if err != nil { + if projected { + wait(attempt) + continue + } + return tokenFileSnapshot{}, tokenFileSnapshot{}, tokenFileSnapshot{}, err + } + previous, err := readTokenFile(readPaths[1], true, maxBearerTokenBytes) + if err != nil { + clear(current.contents) + if projected { + wait(attempt) + continue + } + return tokenFileSnapshot{}, tokenFileSnapshot{}, tokenFileSnapshot{}, err + } + validUntil, err := readTokenFile(readPaths[2], true, maxTokenDeadlineBytes) + if err != nil { + clear(current.contents) + clear(previous.contents) + if projected { + wait(attempt) + continue + } + return tokenFileSnapshot{}, tokenFileSnapshot{}, tokenFileSnapshot{}, err + } + if credentialReadUnchanged(configuredPaths, readPaths, generation, projected, current, previous, validUntil) { + return current, previous, validUntil, nil + } + clear(current.contents) + clear(previous.contents) + clear(validUntil.contents) + wait(attempt) + } + return tokenFileSnapshot{}, tokenFileSnapshot{}, tokenFileSnapshot{}, errTokenReload +} + +func waitForNextTokenFileRead(attempt int) { + if attempt+1 < tokenFileReadAttempts { + time.Sleep(tokenFileReadRetryDelay) + } +} + +func credentialReadPaths(configured [3]string) ([3]string, string, bool, error) { + if configured[1] == "" || configured[2] == "" { + return configured, "", false, nil + } + directory := filepath.Dir(configured[0]) + if filepath.Dir(configured[1]) != directory || filepath.Dir(configured[2]) != directory { + return configured, "", false, nil + } + generationLink := filepath.Join(directory, "..data") + generation, err := os.Readlink(generationLink) + if errors.Is(err, os.ErrNotExist) { + return configured, "", false, nil + } + if err != nil { + return [3]string{}, "", false, err + } + generationDirectory := generation + if !filepath.IsAbs(generationDirectory) { + generationDirectory = filepath.Join(directory, generationDirectory) + } + generationDirectory = filepath.Clean(generationDirectory) + relative, err := filepath.Rel(directory, generationDirectory) + if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return [3]string{}, "", false, errTokenReload + } + info, err := os.Stat(generationDirectory) + if err != nil || !info.IsDir() { + return [3]string{}, "", false, errTokenReload + } + return [3]string{ + filepath.Join(generationDirectory, filepath.Base(configured[0])), + filepath.Join(generationDirectory, filepath.Base(configured[1])), + filepath.Join(generationDirectory, filepath.Base(configured[2])), + }, generationDirectory, true, nil +} + +func credentialReadUnchanged( + configured [3]string, + readPaths [3]string, + generation string, + projected bool, + current tokenFileSnapshot, + previous tokenFileSnapshot, + validUntil tokenFileSnapshot, +) bool { + if projected { + nextPaths, nextGeneration, nextProjected, err := credentialReadPaths(configured) + if err != nil || !nextProjected || nextGeneration != generation || nextPaths != readPaths { + return false + } + } + return tokenFileUnchanged(readPaths[0], current) && tokenFileUnchanged(readPaths[1], previous) && + tokenFileUnchanged(readPaths[2], validUntil) +} + +func readTokenFile(path string, optional bool, maxBytes int64) (tokenFileSnapshot, error) { + if path == "" { + return tokenFileSnapshot{}, nil + } + file, err := os.Open(path) + if err != nil { + if optional && errors.Is(err, os.ErrNotExist) { + return tokenFileSnapshot{}, nil + } + return tokenFileSnapshot{}, err + } + defer file.Close() //nolint:errcheck + + contents, err := io.ReadAll(io.LimitReader(file, maxBytes+1)) + if err != nil { + clear(contents) + return tokenFileSnapshot{}, err + } + info, err := file.Stat() + if err != nil || !info.Mode().IsRegular() { + clear(contents) + return tokenFileSnapshot{}, errTokenReload + } + return tokenFileSnapshot{contents: contents, info: info, present: true}, nil +} + +func tokenFileUnchanged(path string, snapshot tokenFileSnapshot) bool { + if path == "" { + return !snapshot.present + } + info, err := os.Stat(path) + if !snapshot.present { + return errors.Is(err, os.ErrNotExist) + } + return err == nil && os.SameFile(snapshot.info, info) && snapshot.info.Size() == info.Size() && + snapshot.info.ModTime().Equal(info.ModTime()) +} diff --git a/cmd/orka-provider-auth-proxy/credentials_test.go b/cmd/orka-provider-auth-proxy/credentials_test.go new file mode 100644 index 000000000..a7d16fb65 --- /dev/null +++ b/cmd/orka-provider-auth-proxy/credentials_test.go @@ -0,0 +1,552 @@ +package main + +import ( + "context" + "crypto/sha256" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + "time" +) + +const ( + testProviderTokenOld = "0123456789abcdef0123456789abcdef" + testProviderTokenNext = "11111111111111111111111111111111" + testProviderTokenNew = "22222222222222222222222222222222" +) + +func TestTokenFileReloaderSupportsLegacySingleTokenFile(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenOld) + + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, time.Minute) + if err := reloader.reload(); err != nil { + t.Fatalf("reload legacy token file: %v", err) + } + if !store.isReady() || !tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("legacy single-token file was not accepted") + } + if tokenAuthorized(store, testProviderTokenNext) { + t.Fatal("unconfigured token was accepted") + } +} + +func TestTokenFileReloaderTrimsMountedCurrentAndPreviousTokens(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, "\n"+testProviderTokenNew+"\r\n") + writeTokenFile(t, previousPath, "\t"+testProviderTokenOld+"\n") + writeTokenDeadline(t, validUntilPath, now.Add(time.Minute)) + + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, time.Minute) + if err := reloader.reload(); err != nil { + t.Fatalf("reload newline-terminated mounted tokens: %v", err) + } + if !tokenAuthorized(store, testProviderTokenNew) || !tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("trimmed current and previous mounted tokens were not accepted") + } + if tokenAuthorized(store, "\n"+testProviderTokenNew) || tokenAuthorized(store, testProviderTokenOld+"\n") { + t.Fatal("authorization accepted whitespace as part of a bearer token") + } +} + +func TestTokenFileReloaderFailsClosedWhenCurrentFileDisappears(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenOld) + + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, time.Minute) + if err := reloader.reload(); err != nil { + t.Fatalf("initial reload: %v", err) + } + if err := os.Remove(currentPath); err != nil { + t.Fatalf("remove current token file: %v", err) + } + if err := reloader.reload(); err == nil { + t.Fatal("reload succeeded after current token file disappeared") + } + if store.isReady() || tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("missing current token file did not disable authentication") + } +} + +func TestTokenFileReloaderFailsClosedAndRecovers(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenOld) + + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, time.Minute) + if err := reloader.reload(); err != nil { + t.Fatalf("initial reload: %v", err) + } + writeTokenFile(t, previousPath, "malformed token material that must never be logged") + writeTokenDeadline(t, validUntilPath, now.Add(time.Minute)) + if err := reloader.reload(); err == nil { + t.Fatal("malformed reload succeeded") + } + if store.isReady() || tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("last-known token remained active after malformed reload") + } + + proxy, err := newProviderAuthProxyWithTokenStore(proxyConfig{UpstreamBaseURL: "http://upstream.example"}, store) + if err != nil { + t.Fatalf("new proxy: %v", err) + } + assertProxyStatus(t, proxy, readinessPath, "", http.StatusServiceUnavailable) + assertProxyStatus(t, proxy, healthPath, "", http.StatusOK) + assertProxyStatus(t, proxy, "/v1/models", testProviderTokenOld, http.StatusUnauthorized) + + now = now.Add(time.Second) + writeTokenFile(t, previousPath, testProviderTokenNext) + writeTokenDeadline(t, validUntilPath, now.Add(time.Minute)) + if err := reloader.reload(); err != nil { + t.Fatalf("recovery reload: %v", err) + } + if !store.isReady() || !tokenAuthorized(store, testProviderTokenOld) || !tokenAuthorized(store, testProviderTokenNext) { + t.Fatal("valid credentials did not recover authentication") + } + + if err := os.Remove(validUntilPath); err != nil { + t.Fatalf("remove validity file: %v", err) + } + if err := reloader.reload(); err == nil { + t.Fatal("previous token without an absolute validity file succeeded") + } + if store.isReady() { + t.Fatal("incomplete previous-token pair did not fail closed") + } +} + +func TestPreviousTokenOverlapExpiresWithoutExtensionAcrossRestart(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenNew) + writeTokenFile(t, previousPath, testProviderTokenOld) + + overlap := 2 * time.Minute + validUntil := now.Add(overlap) + writeTokenDeadline(t, validUntilPath, validUntil) + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, overlap) + if err := reloader.reload(); err != nil { + t.Fatalf("initial reload: %v", err) + } + if !tokenAuthorized(store, testProviderTokenOld) || !tokenAuthorized(store, testProviderTokenNew) { + t.Fatal("current and previous tokens were not both accepted during overlap") + } + + now = validUntil.Add(time.Nanosecond) + if tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("previous token remained accepted after absolute deadline") + } + if err := reloader.reload(); err != nil { + t.Fatalf("unchanged reload: %v", err) + } + if tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("periodic reload extended an unchanged previous token") + } + if !tokenAuthorized(store, testProviderTokenNew) { + t.Fatal("current token expired with previous token") + } + + // Recreate every mounted file to model a fresh Pod/projected-volume + // materialization while preserving the same absolute expiry metadata. + writeTokenFile(t, currentPath, testProviderTokenNew) + writeTokenFile(t, previousPath, testProviderTokenOld) + writeTokenDeadline(t, validUntilPath, validUntil) + restartedStore, restartedReloader := newTestTokenFileReloader( + t, + &now, + currentPath, + previousPath, + validUntilPath, + overlap, + ) + if err := restartedReloader.reload(); err != nil { + t.Fatalf("restart reload: %v", err) + } + if tokenAuthorized(restartedStore, testProviderTokenOld) { + t.Fatal("process restart extended an expired previous token") + } +} + +func TestPreviousTokenDeadlineCannotExceedConfiguredOverlap(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenNew) + writeTokenFile(t, previousPath, testProviderTokenOld) + writeTokenDeadline(t, validUntilPath, now.Add(time.Minute+time.Second)) + + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, time.Minute) + if err := reloader.reload(); err == nil { + t.Fatal("overlong previous-token deadline succeeded") + } + if store.isReady() { + t.Fatal("overlong previous-token deadline did not fail closed") + } +} + +func TestTokenFileReloaderReadsOneProjectedSecretGeneration(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + overlap := 5 * time.Minute + + firstGeneration := filepath.Join(directory, "..2026_07_25_01") + if err := os.Mkdir(firstGeneration, 0o700); err != nil { + t.Fatalf("create first projected generation: %v", err) + } + writeTokenFile(t, filepath.Join(firstGeneration, "token"), testProviderTokenOld) + switchProjectedGeneration(t, directory, filepath.Base(firstGeneration)) + if err := os.Symlink(filepath.Join("..data", "token"), currentPath); err != nil { + t.Fatalf("create visible current-token symlink: %v", err) + } + + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, overlap) + if err := reloader.reload(); err != nil { + t.Fatalf("initial projected reload: %v", err) + } + + secondGeneration := filepath.Join(directory, "..2026_07_25_02") + if err := os.Mkdir(secondGeneration, 0o700); err != nil { + t.Fatalf("create second projected generation: %v", err) + } + writeTokenFile(t, filepath.Join(secondGeneration, "token"), testProviderTokenNew) + writeTokenFile(t, filepath.Join(secondGeneration, "previous-token"), testProviderTokenOld) + writeTokenDeadline(t, filepath.Join(secondGeneration, "previous-token-valid-until"), now.Add(overlap)) + switchProjectedGeneration(t, directory, filepath.Base(secondGeneration)) + + // Kubernetes publishes ..data before creating visible symlinks for newly + // added keys. Reload must still read the complete selected generation. + if _, err := os.Lstat(previousPath); !os.IsNotExist(err) { + t.Fatalf("previous-token visible path unexpectedly exists: %v", err) + } + if err := reloader.reload(); err != nil { + t.Fatalf("rotated projected reload: %v", err) + } + if !tokenAuthorized(store, testProviderTokenNew) || !tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("projected generation was not published as one credential pair") + } +} + +func TestReadStableTokenFilesRetriesProjectedGenerationResolution(t *testing.T) { + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + if err := os.Symlink("..missing-generation", filepath.Join(directory, "..data")); err != nil { + t.Fatalf("create dangling projected generation: %v", err) + } + generation := filepath.Join(directory, "..2026_07_25_retry") + if err := os.Mkdir(generation, 0o700); err != nil { + t.Fatalf("create replacement projected generation: %v", err) + } + writeTokenFile(t, filepath.Join(generation, "token"), testProviderTokenNew) + + waits := 0 + current, previous, validUntil, err := readStableTokenFilesWithWait( + currentPath, + previousPath, + validUntilPath, + func(int) { + waits++ + if waits == 1 { + switchProjectedGeneration(t, directory, filepath.Base(generation)) + } + }, + ) + if err != nil { + t.Fatalf("read replacement projected generation: %v", err) + } + defer clear(current.contents) + defer clear(previous.contents) + defer clear(validUntil.contents) + if waits != 1 { + t.Fatalf("retry waits = %d, want 1", waits) + } + if string(current.contents) != testProviderTokenNew || previous.present || validUntil.present { + t.Fatal("retry did not return the complete replacement generation") + } +} + +func TestTokenRotationProxyFirst(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenOld) + + overlap := 5 * time.Minute + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, overlap) + if err := reloader.reload(); err != nil { + t.Fatalf("initial reload: %v", err) + } + + now = now.Add(time.Minute) + validUntil := now.Add(overlap) + writeTokenFile(t, currentPath, testProviderTokenNew) + writeTokenFile(t, previousPath, testProviderTokenOld) + writeTokenDeadline(t, validUntilPath, validUntil) + if err := reloader.reload(); err != nil { + t.Fatalf("proxy-first reload: %v", err) + } + if !tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("old controller token was rejected after proxy-first rotation") + } + if !tokenAuthorized(store, testProviderTokenNew) { + t.Fatal("new controller token was rejected after proxy-first rotation") + } + + now = validUntil.Add(time.Nanosecond) + if tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("old token remained accepted after proxy-first overlap") + } +} + +func TestTokenRotationControllerFirstWithPreloadedOverlapToken(t *testing.T) { + now := time.Now().UTC() + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenOld) + + overlap := 5 * time.Minute + store, reloader := newTestTokenFileReloader(t, &now, currentPath, previousPath, validUntilPath, overlap) + if err := reloader.reload(); err != nil { + t.Fatalf("initial reload: %v", err) + } + if tokenAuthorized(store, testProviderTokenNext) { + t.Fatal("unstaged next token was accepted") + } + + // Pre-stage the next controller token in the overlap slot, then switch the + // controller before changing which token is designated current by the proxy. + now = now.Add(time.Minute) + writeTokenFile(t, previousPath, testProviderTokenNext) + writeTokenDeadline(t, validUntilPath, now.Add(overlap)) + if err := reloader.reload(); err != nil { + t.Fatalf("pre-stage next token: %v", err) + } + if !tokenAuthorized(store, testProviderTokenOld) || !tokenAuthorized(store, testProviderTokenNext) { + t.Fatal("pre-staged controller-first tokens were not both accepted") + } + + now = now.Add(time.Minute) + validUntil := now.Add(overlap) + writeTokenFile(t, currentPath, testProviderTokenNext) + writeTokenFile(t, previousPath, testProviderTokenOld) + writeTokenDeadline(t, validUntilPath, validUntil) + if !tokenAuthorized(store, testProviderTokenNext) { + t.Fatal("controller-first request failed before the proxy observed the role swap") + } + if err := reloader.reload(); err != nil { + t.Fatalf("normalize controller-first token roles: %v", err) + } + if !tokenAuthorized(store, testProviderTokenNext) || !tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("normalized controller-first tokens were not both accepted") + } + + now = validUntil.Add(time.Nanosecond) + if tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("old token remained accepted after controller-first overlap") + } +} + +func TestBearerTokenStorePublishesCredentialPairsAtomically(t *testing.T) { + now := time.Now().UTC() + store := newBearerTokenStore(func() time.Time { return now }) + validUntil := now.Add(time.Hour) + store.activate([]byte(testProviderTokenOld), []byte(testProviderTokenNext), validUntil) + + oldDigest := sha256.Sum256([]byte(testProviderTokenOld)) + nextDigest := sha256.Sum256([]byte(testProviderTokenNext)) + newDigest := sha256.Sum256([]byte(testProviderTokenNew)) + var invalid atomic.Bool + var readers sync.WaitGroup + for range 8 { + readers.Go(func() { + for range 10_000 { + active := store.snapshot.Load() + oldPair := active.currentDigest == oldDigest && active.previousDigest == nextDigest + newPair := active.currentDigest == newDigest && active.previousDigest == oldDigest + if !active.ready || !active.hasPrevious || (!oldPair && !newPair) { + invalid.Store(true) + return + } + } + }) + } + for range 10_000 { + store.activate([]byte(testProviderTokenNew), []byte(testProviderTokenOld), validUntil) + store.activate([]byte(testProviderTokenOld), []byte(testProviderTokenNext), validUntil) + } + readers.Wait() + if invalid.Load() { + t.Fatal("reader observed a partially published credential pair") + } +} + +func TestPeriodicTokenReloadDoesNotLogTokenMaterial(t *testing.T) { + directory := t.TempDir() + currentPath := filepath.Join(directory, "token") + previousPath := filepath.Join(directory, "previous-token") + validUntilPath := filepath.Join(directory, "previous-token-valid-until") + writeTokenFile(t, currentPath, testProviderTokenOld) + + store := newBearerTokenStore(time.Now) + reloader, err := newTokenFileReloader(tokenFileReloaderConfig{ + CurrentTokenFile: currentPath, + PreviousTokenFile: previousPath, + PreviousTokenValidUntilFile: validUntilPath, + ReloadInterval: 5 * time.Millisecond, + PreviousTokenOverlap: time.Minute, + }, store) + if err != nil { + t.Fatalf("new reloader: %v", err) + } + if err := reloader.reload(); err != nil { + t.Fatalf("initial reload: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + var messagesMu sync.Mutex + var messages []string + go reloader.run(ctx, func(message string) { + messagesMu.Lock() + defer messagesMu.Unlock() + messages = append(messages, message) + }) + + malformed := "malformed token material that must never be logged" + writeTokenFile(t, currentPath, malformed) + eventually(t, time.Second, func() bool { return !store.isReady() }) + if tokenAuthorized(store, testProviderTokenOld) { + t.Fatal("old token remained accepted after periodic malformed reload") + } + + writeTokenFile(t, currentPath, testProviderTokenNew) + eventually(t, time.Second, func() bool { + return store.isReady() && tokenAuthorized(store, testProviderTokenNew) + }) + + eventually(t, time.Second, func() bool { + messagesMu.Lock() + defer messagesMu.Unlock() + joined := strings.Join(messages, "\n") + return strings.Contains(joined, "reload failed") && strings.Contains(joined, "reload recovered") + }) + cancel() + messagesMu.Lock() + joined := strings.Join(messages, "\n") + messagesMu.Unlock() + for _, secret := range []string{malformed, testProviderTokenOld, testProviderTokenNew} { + if strings.Contains(joined, secret) { + t.Fatalf("token material appeared in reload logs: %q", joined) + } + } +} + +func newTestTokenFileReloader( + t *testing.T, + now *time.Time, + currentPath string, + previousPath string, + validUntilPath string, + overlap time.Duration, +) (*bearerTokenStore, *tokenFileReloader) { + t.Helper() + store := newBearerTokenStore(func() time.Time { return *now }) + reloader, err := newTokenFileReloader(tokenFileReloaderConfig{ + CurrentTokenFile: currentPath, + PreviousTokenFile: previousPath, + PreviousTokenValidUntilFile: validUntilPath, + ReloadInterval: time.Second, + PreviousTokenOverlap: overlap, + }, store) + if err != nil { + t.Fatalf("new token file reloader: %v", err) + } + return store, reloader +} + +func writeTokenFile(t *testing.T, path, token string) { + t.Helper() + if err := os.WriteFile(path, []byte(token), 0o600); err != nil { + t.Fatalf("write token file: %v", err) + } +} + +func writeTokenDeadline(t *testing.T, path string, deadline time.Time) { + t.Helper() + if err := os.WriteFile(path, []byte(deadline.UTC().Format(time.RFC3339Nano)), 0o600); err != nil { + t.Fatalf("write token deadline: %v", err) + } +} + +func switchProjectedGeneration(t *testing.T, directory, generation string) { + t.Helper() + temporaryLink := filepath.Join(directory, "..data_tmp") + if err := os.Symlink(generation, temporaryLink); err != nil { + t.Fatalf("create projected generation link: %v", err) + } + if err := os.Rename(temporaryLink, filepath.Join(directory, "..data")); err != nil { + t.Fatalf("publish projected generation: %v", err) + } +} + +func tokenAuthorized(store *bearerTokenStore, token string) bool { + return store.authorized([]string{"Bearer " + token}) +} + +func assertProxyStatus(t *testing.T, proxy *providerAuthProxy, path, token string, expected int) { + t.Helper() + request := httptest.NewRequest(http.MethodGet, "http://proxy"+path, nil) + if token != "" { + request.Header.Set(authorizationHeader, "Bearer "+token) + } + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != expected { + t.Fatalf("%s status = %d, want %d", path, response.Code, expected) + } +} + +func eventually(t *testing.T, timeout time.Duration, condition func() bool) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if condition() { + return + } + time.Sleep(time.Millisecond) + } + t.Fatal("condition was not satisfied before timeout") +} diff --git a/cmd/orka-provider-auth-proxy/main.go b/cmd/orka-provider-auth-proxy/main.go new file mode 100644 index 000000000..633d63f87 --- /dev/null +++ b/cmd/orka-provider-auth-proxy/main.go @@ -0,0 +1,136 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "context" + "flag" + "log" + "net" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" +) + +func main() { + listenAddress := flag.String("listen-address", envDefault("ORKA_PROVIDER_AUTH_PROXY_LISTEN_ADDRESS", ":8080"), "HTTP listen address") + upstreamBaseURL := flag.String("upstream-base-url", os.Getenv("ORKA_PROVIDER_AUTH_PROXY_UPSTREAM_BASE_URL"), "Unauthenticated Vekil upstream base URL") + tokenFile := flag.String("token-file", envDefault("ORKA_PROVIDER_AUTH_PROXY_TOKEN_FILE", "/var/run/secrets/orka/provider-auth/token"), "Mounted current bearer token file") + previousTokenFile := flag.String("previous-token-file", os.Getenv("ORKA_PROVIDER_AUTH_PROXY_PREVIOUS_TOKEN_FILE"), "Optional mounted previous/overlap bearer token file") + previousTokenValidUntilFile := flag.String("previous-token-valid-until-file", os.Getenv("ORKA_PROVIDER_AUTH_PROXY_PREVIOUS_TOKEN_VALID_UNTIL_FILE"), "Optional mounted RFC3339 expiry file for the previous/overlap token") + tokenReloadInterval := flag.Duration("token-reload-interval", envDurationDefault("ORKA_PROVIDER_AUTH_PROXY_TOKEN_RELOAD_INTERVAL", defaultTokenReloadInterval), "Bearer token file reload interval") + previousTokenOverlap := flag.Duration("previous-token-overlap", envDurationDefault("ORKA_PROVIDER_AUTH_PROXY_PREVIOUS_TOKEN_OVERLAP", defaultPreviousTokenOverlap), "Maximum previous/overlap token acceptance window") + maxRequestBytes := flag.Int64("max-request-bytes", envInt64Default("ORKA_PROVIDER_AUTH_PROXY_MAX_REQUEST_BYTES", defaultMaxRequestBytes), "Maximum streamed request body size") + maxResponseBytes := flag.Int64("max-response-bytes", envInt64Default("ORKA_PROVIDER_AUTH_PROXY_MAX_RESPONSE_BYTES", defaultMaxResponseBytes), "Maximum streamed response body size") + responseHeaderTimeout := flag.Duration("response-header-timeout", envDurationDefault("ORKA_PROVIDER_AUTH_PROXY_RESPONSE_HEADER_TIMEOUT", defaultResponseHeaderTimeout), "Upstream response header timeout") + maxConcurrentRequests := flag.Int("max-concurrent-requests", envIntDefault("ORKA_PROVIDER_AUTH_PROXY_MAX_CONCURRENT_REQUESTS", defaultMaxConcurrentRequests), "Maximum concurrent upstream requests") + flag.Parse() + + tokens := newBearerTokenStore(time.Now) + reloader, err := newTokenFileReloader(tokenFileReloaderConfig{ + CurrentTokenFile: *tokenFile, + PreviousTokenFile: *previousTokenFile, + PreviousTokenValidUntilFile: *previousTokenValidUntilFile, + ReloadInterval: *tokenReloadInterval, + PreviousTokenOverlap: *previousTokenOverlap, + }, tokens) + if err != nil { + log.Fatalf("invalid provider auth proxy token reload configuration: %v", err) + } + if err := reloader.reload(); err != nil { + log.Fatal("provider auth proxy token files are unavailable or invalid") + } + proxy, err := newProviderAuthProxyWithTokenStore(proxyConfig{ + UpstreamBaseURL: *upstreamBaseURL, + MaxRequestBytes: *maxRequestBytes, + MaxResponseBytes: *maxResponseBytes, + ResponseHeaderTimeout: *responseHeaderTimeout, + MaxConcurrentRequests: *maxConcurrentRequests, + }, tokens) + if err != nil { + log.Fatalf("invalid provider auth proxy configuration: %v", err) + } + listener, err := net.Listen("tcp", strings.TrimSpace(*listenAddress)) + if err != nil { + log.Fatalf("listen: %v", err) + } + server := newProxyHTTPServer(strings.TrimSpace(*listenAddress), proxy) + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + go reloader.run(ctx, func(message string) { log.Print(message) }) + serveErr := make(chan error, 1) + go func() { + serveErr <- server.Serve(listener) + }() + log.Printf("provider auth proxy listening on %s", listener.Addr()) + select { + case err := <-serveErr: + if err != nil && !errorsIsServerClosed(err) { + log.Fatalf("serve provider auth proxy: %v", err) + } + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + log.Printf("provider auth proxy shutdown failed") + } + if err := <-serveErr; err != nil && !errorsIsServerClosed(err) { + log.Printf("provider auth proxy stopped unexpectedly") + } + } +} + +func errorsIsServerClosed(err error) bool { + return err == http.ErrServerClosed +} + +func envDefault(name, fallback string) string { + if value := strings.TrimSpace(os.Getenv(name)); value != "" { + return value + } + return fallback +} + +func envInt64Default(name string, fallback int64) int64 { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return fallback + } + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil { + log.Fatalf("invalid %s", name) + } + return parsed +} + +func envIntDefault(name string, fallback int) int { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return fallback + } + parsed, err := strconv.Atoi(value) + if err != nil { + log.Fatalf("invalid %s", name) + } + return parsed +} + +func envDurationDefault(name string, fallback time.Duration) time.Duration { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return fallback + } + parsed, err := time.ParseDuration(value) + if err != nil { + log.Fatalf("invalid %s", name) + } + return parsed +} diff --git a/cmd/orka-provider-auth-proxy/proxy.go b/cmd/orka-provider-auth-proxy/proxy.go new file mode 100644 index 000000000..868f02b9c --- /dev/null +++ b/cmd/orka-provider-auth-proxy/proxy.go @@ -0,0 +1,420 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + healthPath = "/healthz" + readinessPath = "/readyz" + defaultMaxRequestBytes = 32 << 20 + defaultMaxResponseBytes = 64 << 20 + defaultResponseHeaderTimeout = 30 * time.Second + defaultReadHeaderTimeout = 5 * time.Second + defaultIdleTimeout = 30 * time.Second + defaultMaxConcurrentRequests = 32 + authorizationHeader = "Authorization" + proxyAuthorizationHeader = "Proxy-Authorization" + providerAPIKeyHeader = "X-Api-Key" + providerLegacyAPIKeyHeader = "Api-Key" + providerContentEncodingHeader = "Content-Encoding" +) + +var errRequestBodyTooLarge = errors.New("provider request body exceeds limit") + +type proxyConfig struct { + UpstreamBaseURL string + MaxRequestBytes int64 + MaxResponseBytes int64 + ResponseHeaderTimeout time.Duration + MaxConcurrentRequests int +} + +type providerAuthProxy struct { + upstreamBase *url.URL + tokens *bearerTokenStore + maxRequestBytes int64 + maxResponseBytes int64 + client *http.Client + requestSlots chan struct{} +} + +func newProviderAuthProxy(cfg proxyConfig, bearerToken []byte) (*providerAuthProxy, error) { + tokens, err := newStaticBearerTokenStore(bearerToken) + if err != nil { + return nil, err + } + return newProviderAuthProxyWithTokenStore(cfg, tokens) +} + +func newProviderAuthProxyWithTokenStore(cfg proxyConfig, tokens *bearerTokenStore) (*providerAuthProxy, error) { + normalized, upstream, err := normalizeProxyConfig(cfg) + if err != nil { + return nil, err + } + if tokens == nil { + return nil, fmt.Errorf("provider auth token store is required") + } + transport := &http.Transport{ + Proxy: nil, + DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext, + ForceAttemptHTTP2: false, + DisableKeepAlives: false, + MaxIdleConns: normalized.MaxConcurrentRequests, + MaxIdleConnsPerHost: normalized.MaxConcurrentRequests, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: normalized.ResponseHeaderTimeout, + ExpectContinueTimeout: time.Second, + MaxResponseHeaderBytes: 64 << 10, + DisableCompression: true, + } + return &providerAuthProxy{ + upstreamBase: upstream, + tokens: tokens, + maxRequestBytes: normalized.MaxRequestBytes, + maxResponseBytes: normalized.MaxResponseBytes, + client: &http.Client{ + Transport: transport, + CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + requestSlots: make(chan struct{}, normalized.MaxConcurrentRequests), + }, nil +} + +func normalizeProxyConfig(cfg proxyConfig) (proxyConfig, *url.URL, error) { + parsed, err := url.Parse(strings.TrimSpace(cfg.UpstreamBaseURL)) + if err != nil || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || + (parsed.Scheme != "http" && parsed.Scheme != "https") { + return proxyConfig{}, nil, fmt.Errorf("provider upstream base URL is invalid") + } + if parsed.Path == "" { + parsed.Path = "/" + } + if hasUnsafePathSegment(parsed.Path) { + return proxyConfig{}, nil, fmt.Errorf("provider upstream base URL is invalid") + } + if cfg.MaxRequestBytes <= 0 { + cfg.MaxRequestBytes = defaultMaxRequestBytes + } + if cfg.MaxResponseBytes <= 0 { + cfg.MaxResponseBytes = defaultMaxResponseBytes + } + if cfg.ResponseHeaderTimeout <= 0 { + cfg.ResponseHeaderTimeout = defaultResponseHeaderTimeout + } + if cfg.MaxConcurrentRequests <= 0 { + cfg.MaxConcurrentRequests = defaultMaxConcurrentRequests + } + cfg.UpstreamBaseURL = parsed.String() + return cfg, parsed, nil +} + +func validateBearerToken(token []byte) error { + if len(token) < 32 || len(token) > maxBearerTokenBytes { + return fmt.Errorf("provider auth bearer token is invalid") + } + for _, value := range token { + if value <= ' ' || value == 0x7f { + return fmt.Errorf("provider auth bearer token is invalid") + } + } + return nil +} + +func (p *providerAuthProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == healthPath { + serveHealth(w, r) + return + } + if r.URL.Path == readinessPath { + if !p.tokens.isReady() { + writeProxyError(w, http.StatusServiceUnavailable, "provider proxy authentication is unavailable") + return + } + serveHealth(w, r) + return + } + if !p.authorized(r.Header.Values(authorizationHeader)) { + w.Header().Set("WWW-Authenticate", `Bearer realm="orka-provider-auth-proxy"`) + writeProxyError(w, http.StatusUnauthorized, "provider proxy authentication required") + return + } + if !tryAcquire(p.requestSlots) { + writeProxyError(w, http.StatusTooManyRequests, "provider proxy request capacity is exhausted") + return + } + defer release(p.requestSlots) + if r.Method == http.MethodConnect || r.Method == http.MethodTrace { + writeProxyError(w, http.StatusMethodNotAllowed, "provider request method is not allowed") + return + } + if hasUnsafePathSegment(r.URL.Path) { + writeProxyError(w, http.StatusBadRequest, "provider request path is invalid") + return + } + if encoding := strings.TrimSpace(r.Header.Get(providerContentEncodingHeader)); encoding != "" && !strings.EqualFold(encoding, "identity") { + writeProxyError(w, http.StatusUnsupportedMediaType, "compressed provider requests are forbidden") + return + } + if r.ContentLength > p.maxRequestBytes { + writeProxyError(w, http.StatusRequestEntityTooLarge, "provider request body exceeds limit") + return + } + + target := providerTarget(p.upstreamBase, r.URL.Path, r.URL.RawQuery) + body := &boundedReadCloser{ReadCloser: r.Body, remaining: p.maxRequestBytes} + upstreamRequest, err := http.NewRequestWithContext(r.Context(), r.Method, target.String(), body) + if err != nil { + writeProxyError(w, http.StatusBadGateway, "provider request could not be prepared") + return + } + upstreamRequest.ContentLength = r.ContentLength + copyProviderRequestHeaders(upstreamRequest.Header, r.Header) + upstreamRequest.Header.Set("Accept-Encoding", "identity") + + response, err := p.client.Do(upstreamRequest) + if err != nil { + if errors.Is(err, errRequestBodyTooLarge) { + writeProxyError(w, http.StatusRequestEntityTooLarge, "provider request body exceeds limit") + return + } + writeProxyError(w, http.StatusBadGateway, "provider upstream request failed") + return + } + defer response.Body.Close() //nolint:errcheck + if response.StatusCode >= http.StatusMultipleChoices && response.StatusCode < http.StatusBadRequest { + writeProxyError(w, http.StatusBadGateway, "provider upstream redirects are forbidden") + return + } + if encoding := strings.TrimSpace(response.Header.Get(providerContentEncodingHeader)); encoding != "" && !strings.EqualFold(encoding, "identity") { + writeProxyError(w, http.StatusBadGateway, "compressed provider responses are forbidden") + return + } + if response.ContentLength > p.maxResponseBytes { + writeProxyError(w, http.StatusBadGateway, "provider upstream response exceeds limit") + return + } + copyProviderResponseHeaders(w.Header(), response.Header) + w.WriteHeader(response.StatusCode) + if err := streamBoundedResponse(w, response.Body, p.maxResponseBytes); err != nil { + panic(http.ErrAbortHandler) + } +} + +func (p *providerAuthProxy) authorized(values []string) bool { + return p.tokens.authorized(values) +} + +func serveHealth(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + writeProxyError(w, http.StatusMethodNotAllowed, "health probe method is not allowed") + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + w.WriteHeader(http.StatusOK) + if r.Method != http.MethodHead { + _, _ = io.WriteString(w, "ok\n") + } +} + +type boundedReadCloser struct { + io.ReadCloser + remaining int64 +} + +func (r *boundedReadCloser) Read(buffer []byte) (int, error) { + if r.remaining < 0 { + return 0, errRequestBodyTooLarge + } + maxRead := min(int64(len(buffer)), r.remaining+1) + n, err := r.ReadCloser.Read(buffer[:maxRead]) + if int64(n) > r.remaining { + allowed := int(r.remaining) + r.remaining = -1 + return allowed, errRequestBodyTooLarge + } + r.remaining -= int64(n) + return n, err +} + +func streamBoundedResponse(destination io.Writer, source io.Reader, limit int64) error { + remaining := limit + buffer := make([]byte, 32<<10) + for { + readSize := len(buffer) + if int64(readSize) > remaining+1 { + readSize = int(remaining + 1) + } + n, readErr := source.Read(buffer[:readSize]) + if int64(n) > remaining { + if remaining > 0 { + if _, writeErr := destination.Write(buffer[:remaining]); writeErr != nil { + return writeErr + } + } + return fmt.Errorf("provider upstream response exceeds limit") + } + if n > 0 { + written, writeErr := destination.Write(buffer[:n]) + remaining -= int64(written) + if writeErr != nil { + return writeErr + } + if written != n { + return io.ErrShortWrite + } + } + if readErr != nil { + if errors.Is(readErr, io.EOF) { + return nil + } + return readErr + } + } +} + +func providerTarget(base *url.URL, requestPath, rawQuery string) *url.URL { + target := *base + target.Path = strings.TrimSuffix(base.Path, "/") + "/" + strings.TrimPrefix(requestPath, "/") + target.RawPath = "" + target.RawQuery = rawQuery + return &target +} + +func hasUnsafePathSegment(path string) bool { + decoded, err := url.PathUnescape(path) + if err != nil { + return true + } + for segment := range strings.SplitSeq(decoded, "/") { + if segment == "." || segment == ".." { + return true + } + } + return false +} + +func copyProviderRequestHeaders(destination, source http.Header) { + blocked := blockedHeaders(source) + for name, values := range source { + canonical := http.CanonicalHeaderKey(name) + if blocked[canonical] || isSensitiveRequestHeader(canonical) { + continue + } + for _, value := range values { + destination.Add(canonical, value) + } + } +} + +func copyProviderResponseHeaders(destination, source http.Header) { + blocked := blockedHeaders(source) + for name, values := range source { + canonical := http.CanonicalHeaderKey(name) + if blocked[canonical] || isSensitiveResponseHeader(canonical) { + continue + } + for _, value := range values { + destination.Add(canonical, value) + } + } +} + +func blockedHeaders(header http.Header) map[string]bool { + blocked := map[string]bool{ + "Connection": true, + "Keep-Alive": true, + "Proxy-Authenticate": true, + proxyAuthorizationHeader: true, + "Proxy-Connection": true, + "Te": true, + "Trailer": true, + "Transfer-Encoding": true, + "Upgrade": true, + } + for _, connection := range header.Values("Connection") { + for name := range strings.SplitSeq(connection, ",") { + name = http.CanonicalHeaderKey(strings.TrimSpace(name)) + if name != "" { + blocked[name] = true + } + } + } + return blocked +} + +func isSensitiveRequestHeader(name string) bool { + switch name { + case authorizationHeader, proxyAuthorizationHeader, providerAPIKeyHeader, providerLegacyAPIKeyHeader, + "Cookie", "Set-Cookie", "Forwarded", "X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto", + "X-Real-Ip", "X-Forwarded-Prefix", "X-Original-Url", "X-Rewrite-Url", "X-Envoy-Original-Path", + "X-Http-Method-Override", "Txn-Token", "Origin", "Referer", "Openai-Organization", "Openai-Project", + "Anthropic-Organization-Id", "Traceparent", "Tracestate", "Baggage", providerContentEncodingHeader, "Expect": + return true + default: + return strings.HasPrefix(name, "X-Orka-") || strings.HasPrefix(name, "X-Forwarded-") || strings.HasPrefix(name, "Sec-Fetch-") + } +} + +func isSensitiveResponseHeader(name string) bool { + switch name { + case authorizationHeader, proxyAuthorizationHeader, providerAPIKeyHeader, providerLegacyAPIKeyHeader, + "Set-Cookie", "Set-Cookie2", "Location", "Server", "Alt-Svc", "Www-Authenticate", "Proxy-Authenticate", + providerContentEncodingHeader: + return true + default: + return false + } +} + +func tryAcquire(slots chan struct{}) bool { + select { + case slots <- struct{}{}: + return true + default: + return false + } +} + +func release(slots chan struct{}) { + <-slots +} + +func writeProxyError(w http.ResponseWriter, status int, message string) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + w.WriteHeader(status) + _, _ = io.WriteString(w, message+"\n") +} + +func newProxyHTTPServer(address string, handler http.Handler) *http.Server { + return &http.Server{ + Addr: address, + Handler: handler, + ReadHeaderTimeout: defaultReadHeaderTimeout, + IdleTimeout: defaultIdleTimeout, + MaxHeaderBytes: 32 << 10, + BaseContext: func(net.Listener) context.Context { + return context.Background() + }, + } +} diff --git a/cmd/orka-provider-auth-proxy/proxy_test.go b/cmd/orka-provider-auth-proxy/proxy_test.go new file mode 100644 index 000000000..11b9d64f3 --- /dev/null +++ b/cmd/orka-provider-auth-proxy/proxy_test.go @@ -0,0 +1,159 @@ +package main + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +const testSharedProviderToken = "0123456789abcdef0123456789abcdef" + +func TestProviderAuthProxyRejectsMissingAndWrongBearerTokens(t *testing.T) { + upstreamCalls := 0 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + upstreamCalls++ + w.WriteHeader(http.StatusNoContent) + })) + t.Cleanup(upstream.Close) + proxy := newTestProxy(t, upstream.URL, testSharedProviderToken) + + for name, authorization := range map[string]string{ + "missing": "", + "wrong": "Bearer 0123456789abcdef0123456789abcdeg", + "basic": "Basic " + testSharedProviderToken, + } { + t.Run(name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodPost, "http://proxy/v1/responses", strings.NewReader(`{"model":"test"}`)) + if authorization != "" { + request.Header.Set(authorizationHeader, authorization) + } + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want %d", response.Code, http.StatusUnauthorized) + } + }) + } + if upstreamCalls != 0 { + t.Fatalf("upstream calls = %d, want 0", upstreamCalls) + } +} + +func TestProviderAuthProxyForwardsAuthorizedRequestWithoutSensitiveHeaders(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/base/v1/responses" || r.URL.RawQuery != "stream=true" { + t.Fatalf("upstream URL = %s, want /base/v1/responses?stream=true", r.URL.String()) + } + for _, name := range []string{authorizationHeader, providerAPIKeyHeader, "Cookie", "Txn-Token", "X-Orka-Internal"} { + if value := r.Header.Get(name); value != "" { + t.Fatalf("upstream received sensitive header %s", name) + } + } + if got := r.Header.Get("Accept-Encoding"); got != "identity" { + t.Fatalf("Accept-Encoding = %q, want identity", got) + } + if got := r.Header.Get("Content-Type"); got != "application/json" { + t.Fatalf("Content-Type = %q", got) + } + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("read upstream request: %v", err) + } + if string(body) != `{"model":"test"}` { + t.Fatalf("upstream body = %q", body) + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Set-Cookie", "secret=value") + _, _ = io.WriteString(w, `{"ok":true}`) + })) + t.Cleanup(upstream.Close) + proxy := newTestProxy(t, upstream.URL+"/base", testSharedProviderToken) + request := httptest.NewRequest(http.MethodPost, "http://proxy/v1/responses?stream=true", strings.NewReader(`{"model":"test"}`)) + request.Header.Set(authorizationHeader, "Bearer "+testSharedProviderToken) + request.Header.Set(providerAPIKeyHeader, "child-key") + request.Header.Set("Cookie", "child=cookie") + request.Header.Set("Txn-Token", "transaction") + request.Header.Set("X-Orka-Internal", "internal") + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusOK || response.Body.String() != `{"ok":true}` { + t.Fatalf("response = %d %q", response.Code, response.Body.String()) + } + if value := response.Header().Get("Set-Cookie"); value != "" { + t.Fatalf("sensitive response header leaked: %q", value) + } +} + +func TestProviderAuthProxyHealthDoesNotRequireAuthentication(t *testing.T) { + proxy := newTestProxy(t, "http://upstream.example", testSharedProviderToken) + for _, path := range []string{healthPath, readinessPath} { + request := httptest.NewRequest(http.MethodGet, "http://proxy"+path, nil) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusOK || response.Body.String() != "ok\n" { + t.Fatalf("%s response = %d %q", path, response.Code, response.Body.String()) + } + } +} + +func TestProviderAuthProxyRejectsRedirectsAndCompressedResponses(t *testing.T) { + for name, handler := range map[string]http.HandlerFunc{ + "redirect": func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", "http://elsewhere.example") + w.WriteHeader(http.StatusTemporaryRedirect) + }, + "compressed": func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set(providerContentEncodingHeader, "gzip") + _, _ = io.WriteString(w, "compressed") + }, + } { + t.Run(name, func(t *testing.T) { + upstream := httptest.NewServer(handler) + t.Cleanup(upstream.Close) + proxy := newTestProxy(t, upstream.URL, testSharedProviderToken) + request := httptest.NewRequest(http.MethodGet, "http://proxy/v1/models", nil) + request.Header.Set(authorizationHeader, "Bearer "+testSharedProviderToken) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusBadGateway { + t.Fatalf("status = %d, want %d", response.Code, http.StatusBadGateway) + } + }) + } +} + +func TestProviderAuthProxyRejectsOversizeKnownRequest(t *testing.T) { + upstreamCalls := 0 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + upstreamCalls++ + w.WriteHeader(http.StatusNoContent) + })) + t.Cleanup(upstream.Close) + proxy, err := newProviderAuthProxy(proxyConfig{UpstreamBaseURL: upstream.URL, MaxRequestBytes: 4}, []byte(testSharedProviderToken)) + if err != nil { + t.Fatalf("new proxy: %v", err) + } + request := httptest.NewRequest(http.MethodPost, "http://proxy/v1/responses", strings.NewReader("12345")) + request.Header.Set(authorizationHeader, "Bearer "+testSharedProviderToken) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("status = %d, want %d", response.Code, http.StatusRequestEntityTooLarge) + } + if upstreamCalls != 0 { + t.Fatalf("upstream calls = %d, want 0", upstreamCalls) + } +} + +//nolint:unparam // The stable parameter keeps call sites explicit across related test cases. +func newTestProxy(t *testing.T, upstreamURL, token string) *providerAuthProxy { + t.Helper() + proxy, err := newProviderAuthProxy(proxyConfig{UpstreamBaseURL: upstreamURL}, []byte(token)) + if err != nil { + t.Fatalf("new provider auth proxy: %v", err) + } + return proxy +} diff --git a/cmd/orka-scm-egress-proxy/auth.go b/cmd/orka-scm-egress-proxy/auth.go new file mode 100644 index 000000000..378e99635 --- /dev/null +++ b/cmd/orka-scm-egress-proxy/auth.go @@ -0,0 +1,89 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "bytes" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" +) + +type proxyAuthenticator struct { + tokenDigest [sha256.Size]byte +} + +func loadProxyAuthenticator(path string) (*proxyAuthenticator, error) { + if !filepath.IsAbs(path) || filepath.Clean(path) != path { + return nil, fmt.Errorf("proxy token path must be absolute and clean") + } + info, err := os.Lstat(path) + if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Mode().Perm()&0o027 != 0 || + info.Size() < minProxyTokenBytes || info.Size() > maxProxyTokenBytes+2 { + return nil, fmt.Errorf("proxy token file is missing or unsafe") + } + value, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read proxy token file: %w", err) + } + value = bytes.TrimSuffix(value, []byte("\r\n")) + value = bytes.TrimSuffix(value, []byte("\n")) + return newProxyAuthenticator(value) +} + +func newProxyAuthenticator(token []byte) (*proxyAuthenticator, error) { + if err := validateProxyToken(token); err != nil { + return nil, err + } + return &proxyAuthenticator{tokenDigest: sha256.Sum256(token)}, nil +} + +func validateProxyToken(token []byte) error { + if len(token) < minProxyTokenBytes || len(token) > maxProxyTokenBytes { + return fmt.Errorf("proxy token length is invalid") + } + for _, current := range token { + if !isProxyTokenCharacter(current) { + return fmt.Errorf("proxy token contains an unsupported character") + } + } + return nil +} + +func isProxyTokenCharacter(value byte) bool { + return value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || + value >= '0' && value <= '9' || strings.ContainsRune("-._~", rune(value)) +} + +func (a *proxyAuthenticator) authorized(request *http.Request) bool { + if a == nil { + return false + } + values := request.Header.Values("Proxy-Authorization") + if len(values) != 1 { + return false + } + const prefix = "Basic " + if !strings.HasPrefix(values[0], prefix) || len(values[0]) > 1024 { + return false + } + decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(values[0], prefix)) + if err != nil || bytes.ContainsAny(decoded, "\r\n\x00") { + return false + } + username, token, found := bytes.Cut(decoded, []byte{':'}) + if !found || string(username) != proxyUsername { + return false + } + digest := sha256.Sum256(token) + return subtle.ConstantTimeCompare(digest[:], a.tokenDigest[:]) == 1 +} diff --git a/cmd/orka-scm-egress-proxy/config.go b/cmd/orka-scm-egress-proxy/config.go new file mode 100644 index 000000000..f485b87da --- /dev/null +++ b/cmd/orka-scm-egress-proxy/config.go @@ -0,0 +1,235 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "context" + "errors" + "fmt" + "net" + "net/netip" + "net/url" + "sort" + "strings" + "time" +) + +const ( + defaultListenAddress = ":8080" + defaultAllowedHosts = "github.com" + defaultForgeAPIBaseURL = "https://api.github.com" + defaultTokenFile = "/var/run/secrets/orka/scm-egress/token" + defaultMaxRequestHeaderBytes = int64(32 << 10) + defaultMaxResponseHeader = int64(64 << 10) + defaultMaxRequestBytes = int64(4 << 20) + defaultMaxResponseBytes = int64(8 << 20) + defaultMaxTunnelBytes = int64(1 << 30) + defaultMaxConcurrent = 8 + defaultResolutionTimeout = 5 * time.Second + defaultConnectTimeout = 10 * time.Second + defaultResponseHeaderTimeout = 30 * time.Second + defaultForwardTimeout = 2 * time.Minute + defaultIdleTimeout = 30 * time.Second + defaultTunnelTimeout = 10 * time.Minute + defaultShutdownTimeout = 15 * time.Second + proxyUsername = "orka-publisher" + maxProxyTokenBytes = 256 + minProxyTokenBytes = 32 + maxResolvedAddresses = 32 +) + +var ( + errHostDenied = errors.New("target host is not allowed") + errAddressDenied = errors.New("target address is not public") + errResolutionFailed = errors.New("target resolution failed") +) + +type resolver interface { + LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) +} + +type contextDialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +type proxyConfig struct { + AllowedHosts map[string]struct{} + MaxRequestHeaderBytes int64 + MaxResponseHeader int64 + MaxRequestBytes int64 + MaxResponseBytes int64 + MaxTunnelBytes int64 + MaxConcurrent int + ResolutionTimeout time.Duration + ConnectTimeout time.Duration + ResponseHeaderTimeout time.Duration + ForwardTimeout time.Duration + IdleTimeout time.Duration + TunnelTimeout time.Duration + Resolver resolver + Dialer contextDialer +} + +func normalizeProxyConfig(config proxyConfig) (proxyConfig, error) { + if len(config.AllowedHosts) == 0 { + return proxyConfig{}, fmt.Errorf("at least one allowed host is required") + } + for host := range config.AllowedHosts { + if err := validateHostname(host); err != nil { + return proxyConfig{}, fmt.Errorf("allowed host is invalid: %w", err) + } + } + applyProxyConfigDefaults(&config) + if err := validateProxyConfigBounds(config); err != nil { + return proxyConfig{}, err + } + if config.Resolver == nil { + config.Resolver = net.DefaultResolver + } + if config.Dialer == nil { + config.Dialer = &net.Dialer{Timeout: config.ConnectTimeout, KeepAlive: -1} + } + return config, nil +} + +func applyProxyConfigDefaults(config *proxyConfig) { + if config.MaxRequestHeaderBytes == 0 { + config.MaxRequestHeaderBytes = defaultMaxRequestHeaderBytes + } + if config.MaxResponseHeader == 0 { + config.MaxResponseHeader = defaultMaxResponseHeader + } + if config.MaxRequestBytes == 0 { + config.MaxRequestBytes = defaultMaxRequestBytes + } + if config.MaxResponseBytes == 0 { + config.MaxResponseBytes = defaultMaxResponseBytes + } + if config.MaxTunnelBytes == 0 { + config.MaxTunnelBytes = defaultMaxTunnelBytes + } + if config.MaxConcurrent == 0 { + config.MaxConcurrent = defaultMaxConcurrent + } + if config.ResolutionTimeout == 0 { + config.ResolutionTimeout = defaultResolutionTimeout + } + if config.ConnectTimeout == 0 { + config.ConnectTimeout = defaultConnectTimeout + } + if config.ResponseHeaderTimeout == 0 { + config.ResponseHeaderTimeout = defaultResponseHeaderTimeout + } + if config.ForwardTimeout == 0 { + config.ForwardTimeout = defaultForwardTimeout + } + if config.IdleTimeout == 0 { + config.IdleTimeout = defaultIdleTimeout + } + if config.TunnelTimeout == 0 { + config.TunnelTimeout = defaultTunnelTimeout + } +} + +func validateProxyConfigBounds(config proxyConfig) error { + if config.MaxRequestHeaderBytes < 1024 || config.MaxRequestHeaderBytes > 1<<20 || + config.MaxResponseHeader < 1024 || config.MaxResponseHeader > 1<<20 || + config.MaxRequestBytes < 1 || config.MaxRequestBytes > 1<<30 || + config.MaxResponseBytes < 1 || config.MaxResponseBytes > 1<<30 || + config.MaxTunnelBytes < 1 || config.MaxTunnelBytes > 16<<30 || + config.MaxConcurrent < 1 || config.MaxConcurrent > 1024 || + config.ResolutionTimeout <= 0 || config.ResolutionTimeout > time.Minute || + config.ConnectTimeout <= 0 || config.ConnectTimeout > time.Minute || + config.ResponseHeaderTimeout <= 0 || config.ResponseHeaderTimeout > 5*time.Minute || + config.ForwardTimeout <= 0 || config.ForwardTimeout > 30*time.Minute || + config.IdleTimeout <= 0 || config.IdleTimeout > 5*time.Minute || + config.TunnelTimeout <= 0 || config.TunnelTimeout > time.Hour { + return fmt.Errorf("proxy limits are invalid") + } + return nil +} + +func allowedHosts(rawHosts, rawForgeAPI string) (map[string]struct{}, error) { + hosts := make(map[string]struct{}) + for raw := range strings.SplitSeq(rawHosts, ",") { + host := strings.TrimSpace(raw) + if host == "" { + continue + } + if err := validateHostname(host); err != nil { + return nil, fmt.Errorf("allowed host is invalid: %w", err) + } + hosts[host] = struct{}{} + } + if strings.TrimSpace(rawForgeAPI) != "" { + forgeHost, err := forgeAPIHostname(rawForgeAPI) + if err != nil { + return nil, err + } + hosts[forgeHost] = struct{}{} + } + if len(hosts) == 0 { + return nil, fmt.Errorf("at least one allowed host is required") + } + return hosts, nil +} + +func forgeAPIHostname(raw string) (string, error) { + if strings.TrimSpace(raw) != raw || len(raw) > 2048 { + return "", fmt.Errorf("forge API base URL is invalid") + } + parsed, err := url.Parse(raw) + if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || + parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Opaque != "" { + return "", fmt.Errorf("forge API base URL is invalid") + } + if parsed.Port() != "" && parsed.Port() != "443" { + return "", fmt.Errorf("forge API base URL must use port 443") + } + host := parsed.Hostname() + if err := validateHostname(host); err != nil { + return "", fmt.Errorf("forge API base URL host is invalid: %w", err) + } + return host, nil +} + +func validateHostname(host string) error { + if host == "" || len(host) > 253 || host != strings.ToLower(host) || strings.HasSuffix(host, ".") || + net.ParseIP(host) != nil { + return fmt.Errorf("hostname must be an exact lower-case DNS name") + } + labels := strings.Split(host, ".") + if len(labels) < 2 { + return fmt.Errorf("hostname must contain at least two labels") + } + for _, label := range labels { + if err := validateHostnameLabel(label); err != nil { + return err + } + } + return nil +} + +func validateHostnameLabel(label string) error { + if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { + return fmt.Errorf("hostname label is invalid") + } + for _, current := range label { + if (current < 'a' || current > 'z') && (current < '0' || current > '9') && current != '-' { + return fmt.Errorf("hostname label contains an unsupported character") + } + } + return nil +} + +func sortedAddresses(addresses []netip.Addr) []netip.Addr { + result := append([]netip.Addr(nil), addresses...) + sort.Slice(result, func(left, right int) bool { + return result[left].Compare(result[right]) < 0 + }) + return result +} diff --git a/cmd/orka-scm-egress-proxy/main.go b/cmd/orka-scm-egress-proxy/main.go new file mode 100644 index 000000000..2bd078c47 --- /dev/null +++ b/cmd/orka-scm-egress-proxy/main.go @@ -0,0 +1,201 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "context" + "errors" + "flag" + "log" + "net" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" +) + +func main() { + listenAddress := flag.String( + "listen-address", + envDefault("ORKA_SCM_EGRESS_PROXY_LISTEN_ADDRESS", defaultListenAddress), + "HTTP proxy listen address", + ) + allowedHostsValue := flag.String( + "allowed-hosts", + envDefault("ORKA_SCM_EGRESS_PROXY_ALLOWED_HOSTS", defaultAllowedHosts), + "Comma-separated exact lower-case SCM hostnames", + ) + forgeAPIBaseURL := flag.String( + "forge-api-base-url", + envDefault("ORKA_SCM_EGRESS_PROXY_FORGE_API_BASE_URL", defaultForgeAPIBaseURL), + "Optional HTTPS forge API base URL whose exact hostname is allowed", + ) + tokenFile := flag.String( + "token-file", + envDefault("ORKA_SCM_EGRESS_PROXY_TOKEN_FILE", defaultTokenFile), + "Publisher proxy-auth token file", + ) + maxRequestHeaderBytes := flag.Int64( + "max-request-header-bytes", + envInt64Default("ORKA_SCM_EGRESS_PROXY_MAX_REQUEST_HEADER_BYTES", defaultMaxRequestHeaderBytes), + "Maximum request header bytes", + ) + maxResponseHeaderBytes := flag.Int64( + "max-response-header-bytes", + envInt64Default("ORKA_SCM_EGRESS_PROXY_MAX_RESPONSE_HEADER_BYTES", defaultMaxResponseHeader), + "Maximum forward response header bytes", + ) + maxRequestBytes := flag.Int64( + "max-request-bytes", + envInt64Default("ORKA_SCM_EGRESS_PROXY_MAX_REQUEST_BYTES", defaultMaxRequestBytes), + "Maximum forward request body bytes", + ) + maxResponseBytes := flag.Int64( + "max-response-bytes", + envInt64Default("ORKA_SCM_EGRESS_PROXY_MAX_RESPONSE_BYTES", defaultMaxResponseBytes), + "Maximum forward response body bytes", + ) + maxTunnelBytes := flag.Int64( + "max-tunnel-bytes", + envInt64Default("ORKA_SCM_EGRESS_PROXY_MAX_TUNNEL_BYTES", defaultMaxTunnelBytes), + "Maximum bytes in each CONNECT tunnel direction", + ) + maxConcurrent := flag.Int( + "max-concurrent", + envIntDefault("ORKA_SCM_EGRESS_PROXY_MAX_CONCURRENT", defaultMaxConcurrent), + "Maximum concurrent requests and tunnels", + ) + resolutionTimeout := flag.Duration( + "resolution-timeout", + envDurationDefault("ORKA_SCM_EGRESS_PROXY_RESOLUTION_TIMEOUT", defaultResolutionTimeout), + "Per-request DNS resolution timeout", + ) + connectTimeout := flag.Duration( + "connect-timeout", + envDurationDefault("ORKA_SCM_EGRESS_PROXY_CONNECT_TIMEOUT", defaultConnectTimeout), + "Per-address TCP connection timeout", + ) + responseHeaderTimeout := flag.Duration( + "response-header-timeout", + envDurationDefault("ORKA_SCM_EGRESS_PROXY_RESPONSE_HEADER_TIMEOUT", defaultResponseHeaderTimeout), + "Forward response header timeout", + ) + forwardTimeout := flag.Duration( + "forward-timeout", + envDurationDefault("ORKA_SCM_EGRESS_PROXY_FORWARD_TIMEOUT", defaultForwardTimeout), + "Maximum complete forward request lifetime", + ) + idleTimeout := flag.Duration( + "idle-timeout", + envDurationDefault("ORKA_SCM_EGRESS_PROXY_IDLE_TIMEOUT", defaultIdleTimeout), + "Connection and tunnel idle timeout", + ) + tunnelTimeout := flag.Duration( + "tunnel-timeout", + envDurationDefault("ORKA_SCM_EGRESS_PROXY_TUNNEL_TIMEOUT", defaultTunnelTimeout), + "Maximum CONNECT tunnel lifetime", + ) + shutdownTimeout := flag.Duration( + "shutdown-timeout", + envDurationDefault("ORKA_SCM_EGRESS_PROXY_SHUTDOWN_TIMEOUT", defaultShutdownTimeout), + "Graceful shutdown timeout", + ) + flag.Parse() + + hosts, err := allowedHosts(*allowedHostsValue, *forgeAPIBaseURL) + if err != nil { + log.Fatal("invalid SCM egress host policy") + } + authenticator, err := loadProxyAuthenticator(strings.TrimSpace(*tokenFile)) + if err != nil { + log.Fatal("SCM egress proxy authentication is unavailable") + } + proxy, err := newSCMEgressProxy(proxyConfig{ + AllowedHosts: hosts, MaxRequestHeaderBytes: *maxRequestHeaderBytes, + MaxResponseHeader: *maxResponseHeaderBytes, MaxRequestBytes: *maxRequestBytes, + MaxResponseBytes: *maxResponseBytes, MaxTunnelBytes: *maxTunnelBytes, + MaxConcurrent: *maxConcurrent, ResolutionTimeout: *resolutionTimeout, + ConnectTimeout: *connectTimeout, ResponseHeaderTimeout: *responseHeaderTimeout, + ForwardTimeout: *forwardTimeout, IdleTimeout: *idleTimeout, TunnelTimeout: *tunnelTimeout, + }, authenticator) + if err != nil { + log.Fatal("invalid SCM egress proxy configuration") + } + listener, err := net.Listen("tcp", strings.TrimSpace(*listenAddress)) + if err != nil { + log.Fatal("SCM egress proxy listener is unavailable") + } + server := &http.Server{ + Addr: strings.TrimSpace(*listenAddress), Handler: proxy, + ReadHeaderTimeout: min(*idleTimeout, 10*time.Second), ReadTimeout: *idleTimeout, + WriteTimeout: *responseHeaderTimeout, IdleTimeout: *idleTimeout, + MaxHeaderBytes: int(*maxRequestHeaderBytes), + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + serveResult := make(chan error, 1) + go func() { serveResult <- server.Serve(listener) }() + log.Printf("SCM egress proxy listening on %s", listener.Addr()) + select { + case err := <-serveResult: + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatal("SCM egress proxy stopped unexpectedly") + } + case <-ctx.Done(): + shutdownContext, cancel := context.WithTimeout(context.Background(), *shutdownTimeout) + defer cancel() + if err := server.Shutdown(shutdownContext); err != nil { + log.Print("SCM egress proxy shutdown timed out") + } + } +} + +func envDefault(name, fallback string) string { + if value := strings.TrimSpace(os.Getenv(name)); value != "" { + return value + } + return fallback +} + +func envInt64Default(name string, fallback int64) int64 { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return fallback + } + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil || strconv.FormatInt(parsed, 10) != value { + log.Fatalf("invalid %s", name) + } + return parsed +} + +func envIntDefault(name string, fallback int) int { + raw := strings.TrimSpace(os.Getenv(name)) + if raw == "" { + return fallback + } + value, err := strconv.Atoi(raw) + if err != nil || value < 1 || strconv.Itoa(value) != raw { + log.Fatalf("invalid %s", name) + } + return value +} + +func envDurationDefault(name string, fallback time.Duration) time.Duration { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return fallback + } + parsed, err := time.ParseDuration(value) + if err != nil { + log.Fatalf("invalid %s", name) + } + return parsed +} diff --git a/cmd/orka-scm-egress-proxy/network.go b/cmd/orka-scm-egress-proxy/network.go new file mode 100644 index 000000000..72ed0e4ec --- /dev/null +++ b/cmd/orka-scm-egress-proxy/network.go @@ -0,0 +1,162 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "context" + "errors" + "fmt" + "net" + "net/netip" + "strconv" +) + +var deniedAddressPrefixes = mustPrefixes( + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.2.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + "::/128", + "::1/128", + "64:ff9b::/96", + "64:ff9b:1::/48", + "100::/64", + "2001::/32", + "2001:db8::/32", + "2002::/16", + "fc00::/7", + "fe80::/10", + "ff00::/8", +) + +func mustPrefixes(values ...string) []netip.Prefix { + result := make([]netip.Prefix, 0, len(values)) + for _, value := range values { + result = append(result, netip.MustParsePrefix(value)) + } + return result +} + +func (p *scmEgressProxy) dialAllowedHost(ctx context.Context, network, address string) (net.Conn, error) { + host, port, err := splitTarget(address) + if err != nil { + return nil, err + } + if !p.hostAllowed(host) { + return nil, errHostDenied + } + resolutionContext, cancel := context.WithTimeout(ctx, p.config.ResolutionTimeout) + addresses, lookupErr := p.config.Resolver.LookupNetIP(resolutionContext, "ip", host) + cancel() + if lookupErr != nil || len(addresses) == 0 { + return nil, errResolutionFailed + } + addresses = sortedAddresses(addresses) + if len(addresses) > maxResolvedAddresses { + return nil, errResolutionFailed + } + if err := validateResolvedAddresses(addresses); err != nil { + return nil, err + } + connectionContext, cancel := context.WithTimeout(ctx, p.config.ConnectTimeout) + defer cancel() + return p.connectResolved(connectionContext, network, port, addresses) +} + +func splitTarget(address string) (string, string, error) { + host, port, err := net.SplitHostPort(address) + if err != nil || port != "443" { + return "", "", errHostDenied + } + if err := validateHostname(host); err != nil { + return "", "", errHostDenied + } + return host, port, nil +} + +func validateResolvedAddresses(addresses []netip.Addr) error { + for _, address := range addresses { + if !publicAddress(address) { + return errAddressDenied + } + } + return nil +} + +func publicAddress(address netip.Addr) bool { + if !address.IsValid() { + return false + } + address = address.Unmap() + if !address.IsGlobalUnicast() || address.IsPrivate() || address.IsLoopback() || + address.IsLinkLocalUnicast() || address.IsLinkLocalMulticast() || address.IsMulticast() || + address.IsUnspecified() { + return false + } + for _, prefix := range deniedAddressPrefixes { + if prefix.Contains(address) { + return false + } + } + return true +} + +func (p *scmEgressProxy) connectResolved( + ctx context.Context, + network string, + port string, + addresses []netip.Addr, +) (net.Conn, error) { + var failures []error + for _, address := range addresses { + connection, err := p.config.Dialer.DialContext( + ctx, + network, + net.JoinHostPort(address.String(), port), + ) + if err != nil { + failures = append(failures, err) + continue + } + if err := validateConnectedPeer(connection, address); err != nil { + _ = connection.Close() + return nil, err + } + return connection, nil + } + return nil, fmt.Errorf("connect to allowed target: %w", errors.Join(failures...)) +} + +func validateConnectedPeer(connection net.Conn, expected netip.Addr) error { + host, _, err := net.SplitHostPort(connection.RemoteAddr().String()) + if err != nil { + return errAddressDenied + } + actual, err := netip.ParseAddr(host) + if err != nil { + return errAddressDenied + } + actual = actual.Unmap() + if !publicAddress(actual) || actual != expected.Unmap() { + return errAddressDenied + } + return nil +} + +func targetAddress(host string) string { + return net.JoinHostPort(host, strconv.Itoa(443)) +} diff --git a/cmd/orka-scm-egress-proxy/proxy.go b/cmd/orka-scm-egress-proxy/proxy.go new file mode 100644 index 000000000..fa63fe8a2 --- /dev/null +++ b/cmd/orka-scm-egress-proxy/proxy.go @@ -0,0 +1,421 @@ +/* +Copyright (c) 2026. + +MIT License - see LICENSE file for details. +*/ + +package main + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/textproto" + "net/url" + "strings" + "time" +) + +const ( + healthPath = "/healthz" + readinessPath = "/readyz" +) + +var ( + errRedirectDenied = errors.New("upstream redirects are denied") + errTunnelLimit = errors.New("tunnel byte limit exceeded") + errRequestTooLarge = errors.New("request body limit exceeded") +) + +type scmEgressProxy struct { + config proxyConfig + authenticator *proxyAuthenticator + client *http.Client + requestSlots chan struct{} +} + +func newSCMEgressProxy(config proxyConfig, authenticator *proxyAuthenticator) (*scmEgressProxy, error) { + normalized, err := normalizeProxyConfig(config) + if err != nil { + return nil, err + } + if authenticator == nil { + return nil, fmt.Errorf("proxy authenticator is required") + } + proxy := &scmEgressProxy{ + config: normalized, authenticator: authenticator, + requestSlots: make(chan struct{}, normalized.MaxConcurrent), + } + proxy.client = proxy.newForwardClient(nil) + return proxy, nil +} + +func (p *scmEgressProxy) newForwardClient(tlsConfig *tls.Config) *http.Client { + transport := &http.Transport{ + Proxy: nil, + DialContext: p.dialAllowedHost, + ForceAttemptHTTP2: false, + DisableKeepAlives: true, + DisableCompression: true, + TLSClientConfig: tlsConfig, + TLSHandshakeTimeout: p.config.ConnectTimeout, + ResponseHeaderTimeout: p.config.ResponseHeaderTimeout, + ExpectContinueTimeout: time.Second, + MaxResponseHeaderBytes: p.config.MaxResponseHeader, + } + return &http.Client{ + Transport: transport, + Timeout: p.config.ForwardTimeout, + CheckRedirect: func(*http.Request, []*http.Request) error { + return errRedirectDenied + }, + } +} + +func (p *scmEgressProxy) hostAllowed(host string) bool { + _, allowed := p.config.AllowedHosts[host] + return allowed +} + +func (p *scmEgressProxy) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if p.serveProbe(writer, request) { + return + } + if !p.authenticator.authorized(request) { + writer.Header().Set("Proxy-Authenticate", `Basic realm="orka-scm-egress"`) + writeProxyError(writer, http.StatusProxyAuthRequired, "proxy authentication required") + return + } + if requestHeaderBytes(request) > p.config.MaxRequestHeaderBytes { + writeProxyError(writer, http.StatusRequestHeaderFieldsTooLarge, "request headers exceed proxy limit") + return + } + if !tryAcquire(p.requestSlots) { + writeProxyError(writer, http.StatusTooManyRequests, "proxy capacity is exhausted") + return + } + defer releaseSlot(p.requestSlots) + if request.Method == http.MethodConnect { + p.handleConnect(writer, request) + return + } + p.handleForward(writer, request) +} + +func (p *scmEgressProxy) serveProbe(writer http.ResponseWriter, request *http.Request) bool { + if request.Method != http.MethodGet || request.URL.IsAbs() { + return false + } + if request.URL.Path != healthPath && request.URL.Path != readinessPath { + return false + } + writer.Header().Set("Cache-Control", "no-store") + writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + writer.Header().Set("X-Content-Type-Options", "nosniff") + writer.WriteHeader(http.StatusOK) + _, _ = io.WriteString(writer, "ok\n") + return true +} + +func (p *scmEgressProxy) handleConnect(writer http.ResponseWriter, request *http.Request) { + if request.ContentLength > 0 || request.TransferEncoding != nil { + writeProxyError(writer, http.StatusBadRequest, "CONNECT request body is forbidden") + return + } + host, port, err := splitTarget(request.Host) + if err != nil || !p.hostAllowed(host) { + writeProxyError(writer, http.StatusForbidden, "target is not allowed") + return + } + upstream, err := p.dialAllowedHost(request.Context(), "tcp", net.JoinHostPort(host, port)) + if err != nil { + writeDialError(writer, err) + return + } + hijacker, ok := writer.(http.Hijacker) + if !ok { + _ = upstream.Close() + writeProxyError(writer, http.StatusInternalServerError, "CONNECT is unavailable") + return + } + client, buffered, err := hijacker.Hijack() + if err != nil { + _ = upstream.Close() + return + } + if err := client.SetDeadline(time.Time{}); err != nil { + _ = client.Close() + _ = upstream.Close() + return + } + if err := upstream.SetDeadline(time.Time{}); err != nil { + _ = client.Close() + _ = upstream.Close() + return + } + p.runTunnel(client, upstream, buffered) +} + +func (p *scmEgressProxy) runTunnel(client, upstream net.Conn, buffered *bufio.ReadWriter) { + defer func() { _ = client.Close() }() + defer func() { _ = upstream.Close() }() + if _, err := buffered.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil { + return + } + if err := buffered.Flush(); err != nil { + return + } + clientBudget := p.config.MaxTunnelBytes + if buffered.Reader.Buffered() > 0 { + count, err := copyBuffered(upstream, buffered.Reader, clientBudget, p.config.IdleTimeout) + if err != nil { + return + } + clientBudget -= count + } + p.copyTunnel(client, upstream, clientBudget) +} + +func copyBuffered(destination net.Conn, reader *bufio.Reader, limit int64, idleTimeout time.Duration) (int64, error) { + buffered := reader.Buffered() + if int64(buffered) > limit { + return 0, errTunnelLimit + } + data := make([]byte, buffered) + if _, err := io.ReadFull(reader, data); err != nil { + return 0, err + } + deadline := &deadlineConn{Conn: destination, idleTimeout: idleTimeout} + written, err := io.CopyBuffer(deadline, bytes.NewReader(data), make([]byte, 32<<10)) + return written, err +} + +func (p *scmEgressProxy) copyTunnel(client, upstream net.Conn, clientBudget int64) { + results := make(chan error, 2) + go func() { + results <- boundedTunnelCopy( + &deadlineConn{Conn: upstream, idleTimeout: p.config.IdleTimeout}, + &deadlineConn{Conn: client, idleTimeout: p.config.IdleTimeout}, + clientBudget, + ) + }() + go func() { + results <- boundedTunnelCopy( + &deadlineConn{Conn: client, idleTimeout: p.config.IdleTimeout}, + &deadlineConn{Conn: upstream, idleTimeout: p.config.IdleTimeout}, + p.config.MaxTunnelBytes, + ) + }() + timer := time.NewTimer(p.config.TunnelTimeout) + defer timer.Stop() + select { + case <-results: + case <-timer.C: + } +} + +func boundedTunnelCopy(destination io.Writer, source io.Reader, limit int64) error { + written, err := io.CopyBuffer(destination, io.LimitReader(source, limit+1), make([]byte, 32<<10)) + if written > limit { + return errTunnelLimit + } + return err +} + +type deadlineConn struct { + net.Conn + idleTimeout time.Duration +} + +func (c *deadlineConn) Read(value []byte) (int, error) { + if err := c.SetReadDeadline(time.Now().Add(c.idleTimeout)); err != nil { + return 0, err + } + return c.Conn.Read(value) +} + +func (c *deadlineConn) Write(value []byte) (int, error) { + if err := c.SetWriteDeadline(time.Now().Add(c.idleTimeout)); err != nil { + return 0, err + } + return c.Conn.Write(value) +} + +func (p *scmEgressProxy) handleForward(writer http.ResponseWriter, request *http.Request) { + target, err := forwardTarget(request) + if err != nil || !p.hostAllowed(target.Hostname()) { + writeProxyError(writer, http.StatusForbidden, "target is not allowed") + return + } + body, err := readRequestBody(request, p.config.MaxRequestBytes) + if err != nil { + status := http.StatusBadRequest + if errors.Is(err, errRequestTooLarge) { + status = http.StatusRequestEntityTooLarge + } + writeProxyError(writer, status, "request body is invalid or exceeds proxy limit") + return + } + outbound := outboundRequest(request, target, body) + response, err := p.client.Do(outbound) + if err != nil { + writeForwardError(writer, err) + return + } + defer func() { _ = response.Body.Close() }() + if response.StatusCode >= http.StatusMultipleChoices && response.StatusCode < http.StatusBadRequest { + writeProxyError(writer, http.StatusBadGateway, "upstream redirect is forbidden") + return + } + if responseHeaderBytes(response.Header) > p.config.MaxResponseHeader { + writeProxyError(writer, http.StatusBadGateway, "upstream response headers exceed proxy limit") + return + } + responseBody, err := readBounded(response.Body, p.config.MaxResponseBytes) + if err != nil { + writeProxyError(writer, http.StatusBadGateway, "upstream response exceeds proxy limit") + return + } + copyResponseHeaders(writer.Header(), response.Header) + writer.WriteHeader(response.StatusCode) + _, _ = writer.Write(responseBody) +} + +func forwardTarget(request *http.Request) (*url.URL, error) { + if request.Method == http.MethodTrace || !request.URL.IsAbs() || request.URL.Scheme != "https" || + request.URL.User != nil || request.URL.Fragment != "" || request.URL.Opaque != "" { + return nil, errHostDenied + } + if request.URL.Port() != "" && request.URL.Port() != "443" { + return nil, errHostDenied + } + if err := validateHostname(request.URL.Hostname()); err != nil { + return nil, errHostDenied + } + target := *request.URL + target.Host = targetAddress(target.Hostname()) + return &target, nil +} + +func readRequestBody(request *http.Request, limit int64) ([]byte, error) { + if request.ContentLength > limit { + return nil, errRequestTooLarge + } + if request.Body == nil { + return nil, nil + } + defer func() { _ = request.Body.Close() }() + return readBounded(request.Body, limit) +} + +func readBounded(reader io.Reader, limit int64) ([]byte, error) { + data, err := io.ReadAll(io.LimitReader(reader, limit+1)) + if err != nil { + return nil, err + } + if int64(len(data)) > limit { + return nil, errRequestTooLarge + } + return data, nil +} + +func outboundRequest(request *http.Request, target *url.URL, body []byte) *http.Request { + outbound := request.Clone(request.Context()) + outbound.URL = target + outbound.RequestURI = "" + outbound.Host = target.Hostname() + outbound.Header = request.Header.Clone() + stripHopByHopHeaders(outbound.Header) + outbound.Header.Del("Proxy-Authorization") + outbound.Header.Del("Content-Length") + outbound.Body = io.NopCloser(bytes.NewReader(body)) + outbound.ContentLength = int64(len(body)) + outbound.TransferEncoding = nil + outbound.Trailer = nil + return outbound +} + +func stripHopByHopHeaders(header http.Header) { + for _, value := range header.Values("Connection") { + for token := range strings.SplitSeq(value, ",") { + header.Del(textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(token))) + } + } + for _, name := range []string{ + "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Proxy-Connection", "Te", "Trailer", + "Transfer-Encoding", "Upgrade", + } { + header.Del(name) + } +} + +func copyResponseHeaders(destination, source http.Header) { + cloned := source.Clone() + stripHopByHopHeaders(cloned) + for name, values := range cloned { + for _, value := range values { + destination.Add(name, value) + } + } +} + +func requestHeaderBytes(request *http.Request) int64 { + return int64(len(request.Method) + len(request.Host) + len(request.URL.String()) + 4 + headerBytes(request.Header)) +} + +func responseHeaderBytes(header http.Header) int64 { return int64(headerBytes(header)) } + +func headerBytes(header http.Header) int { + total := 0 + for name, values := range header { + for _, value := range values { + total += len(name) + len(value) + 4 + } + } + return total +} + +func writeDialError(writer http.ResponseWriter, err error) { + if errors.Is(err, errHostDenied) || errors.Is(err, errAddressDenied) { + writeProxyError(writer, http.StatusForbidden, "target is not allowed") + return + } + if errors.Is(err, context.DeadlineExceeded) { + writeProxyError(writer, http.StatusGatewayTimeout, "target connection timed out") + return + } + writeProxyError(writer, http.StatusBadGateway, "target connection failed") +} + +func writeForwardError(writer http.ResponseWriter, err error) { + if errors.Is(err, errRedirectDenied) { + writeProxyError(writer, http.StatusBadGateway, "upstream redirect is forbidden") + return + } + writeDialError(writer, err) +} + +func writeProxyError(writer http.ResponseWriter, status int, message string) { + writer.Header().Set("Cache-Control", "no-store") + writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + writer.Header().Set("X-Content-Type-Options", "nosniff") + writer.WriteHeader(status) + _, _ = io.WriteString(writer, message+"\n") +} + +func tryAcquire(slots chan struct{}) bool { + select { + case slots <- struct{}{}: + return true + default: + return false + } +} + +func releaseSlot(slots chan struct{}) { <-slots } diff --git a/cmd/orka-scm-egress-proxy/proxy_test.go b/cmd/orka-scm-egress-proxy/proxy_test.go new file mode 100644 index 000000000..de333af7c --- /dev/null +++ b/cmd/orka-scm-egress-proxy/proxy_test.go @@ -0,0 +1,411 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/netip" + "strings" + "sync/atomic" + "testing" + "time" +) + +const testProxyToken = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" + +func TestSCMEgressProxyRejectsUnlistedProviderHost(t *testing.T) { + var lookups atomic.Int32 + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + Resolver: resolverFunc(func(context.Context, string, string) ([]netip.Addr, error) { + lookups.Add(1) + return []netip.Addr{netip.MustParseAddr("1.1.1.1")}, nil + }), + }) + request := connectRequest("api.openai.com:443") + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusForbidden { + t.Fatalf("status = %d, want %d", response.Code, http.StatusForbidden) + } + if lookups.Load() != 0 { + t.Fatalf("DNS lookups = %d, want 0", lookups.Load()) + } +} + +func TestSCMEgressProxyRejectsPrivateAndRebindingAnswers(t *testing.T) { + for name, addresses := range map[string][]netip.Addr{ + "private": { + netip.MustParseAddr("10.0.0.8"), + }, + "loopback": { + netip.MustParseAddr("127.0.0.1"), + }, + "metadata": { + netip.MustParseAddr("169.254.169.254"), + }, + "mixed-public-private": { + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("192.168.1.7"), + }, + } { + t.Run(name, func(t *testing.T) { + var dials atomic.Int32 + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + Resolver: resolverFunc(func(context.Context, string, string) ([]netip.Addr, error) { + return addresses, nil + }), + Dialer: dialerFunc(func(context.Context, string, string) (net.Conn, error) { + dials.Add(1) + return nil, fmt.Errorf("unexpected dial") + }), + }) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, connectRequest("github.com:443")) + if response.Code != http.StatusForbidden { + t.Fatalf("status = %d, want %d", response.Code, http.StatusForbidden) + } + if dials.Load() != 0 { + t.Fatalf("dials = %d, want 0", dials.Load()) + } + }) + } +} + +func TestSCMEgressProxyAllowsAuthenticatedGitHubCONNECT(t *testing.T) { + upstream, upstreamAddress := startEchoServer(t) + defer func() { _ = upstream.Close() }() + publicAddress := netip.MustParseAddr("1.1.1.1") + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + Resolver: resolverFunc(func(_ context.Context, network, host string) ([]netip.Addr, error) { + if network != "ip" || host != "github.com" { + t.Fatalf("lookup = %s %s", network, host) + } + return []netip.Addr{publicAddress}, nil + }), + Dialer: localTestDialer(t, upstreamAddress, publicAddress), + }) + server := httptest.NewServer(proxy) + defer server.Close() + + connection, err := net.DialTimeout("tcp", strings.TrimPrefix(server.URL, "http://"), time.Second) + if err != nil { + t.Fatalf("dial proxy: %v", err) + } + defer func() { _ = connection.Close() }() + if _, err := fmt.Fprintf( + connection, + "CONNECT github.com:443 HTTP/1.1\r\nHost: github.com:443\r\nProxy-Authorization: %s\r\n\r\n", + proxyAuthorization(), + ); err != nil { + t.Fatalf("write CONNECT: %v", err) + } + reader := bufio.NewReader(connection) + status, err := reader.ReadString('\n') + if err != nil { + t.Fatalf("read CONNECT status: %v", err) + } + if status != "HTTP/1.1 200 Connection Established\r\n" { + t.Fatalf("CONNECT status = %q", status) + } + for { + line, readErr := reader.ReadString('\n') + if readErr != nil { + t.Fatalf("read CONNECT header: %v", readErr) + } + if line == "\r\n" { + break + } + } + if _, err := connection.Write([]byte("github-connect-ok")); err != nil { + t.Fatalf("write tunnel payload: %v", err) + } + payload := make([]byte, len("github-connect-ok")) + if _, err := io.ReadFull(reader, payload); err != nil { + t.Fatalf("read tunnel payload: %v", err) + } + if string(payload) != "github-connect-ok" { + t.Fatalf("tunnel payload = %q", payload) + } +} + +func TestSCMEgressProxyRejectsRedirect(t *testing.T) { + upstream := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + writer.Header().Set("Location", "https://github.com/redirected") + writer.WriteHeader(http.StatusFound) + })) + defer upstream.Close() + proxy := testForwardProxy(t, upstream, 1024, 1024) + request := httptest.NewRequest(http.MethodGet, "https://github.com/start", nil) + request.Header.Set("Proxy-Authorization", proxyAuthorization()) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusBadGateway { + t.Fatalf("status = %d, want %d", response.Code, http.StatusBadGateway) + } +} + +func TestSCMEgressProxyRejectsOversizedHeaders(t *testing.T) { + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + MaxRequestHeaderBytes: 1024, + }) + request := connectRequest("github.com:443") + request.Header.Set("X-Oversized", strings.Repeat("x", 2048)) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusRequestHeaderFieldsTooLarge { + t.Fatalf("status = %d, want %d", response.Code, http.StatusRequestHeaderFieldsTooLarge) + } +} + +func TestSCMEgressProxyRejectsOversizedRequestsAndResponses(t *testing.T) { + t.Run("known request", func(t *testing.T) { + var lookups atomic.Int32 + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + MaxRequestBytes: 4, + Resolver: resolverFunc(func(context.Context, string, string) ([]netip.Addr, error) { + lookups.Add(1) + return []netip.Addr{netip.MustParseAddr("1.1.1.1")}, nil + }), + }) + request := httptest.NewRequest(http.MethodPost, "https://github.com/upload", strings.NewReader("12345")) + request.Header.Set("Proxy-Authorization", proxyAuthorization()) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("status = %d, want %d", response.Code, http.StatusRequestEntityTooLarge) + } + if lookups.Load() != 0 { + t.Fatalf("DNS lookups = %d, want 0", lookups.Load()) + } + }) + + t.Run("streamed request", func(t *testing.T) { + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + MaxRequestBytes: 4, + }) + request := httptest.NewRequest(http.MethodPost, "https://github.com/upload", strings.NewReader("12345")) + request.ContentLength = -1 + request.Header.Set("Proxy-Authorization", proxyAuthorization()) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("status = %d, want %d", response.Code, http.StatusRequestEntityTooLarge) + } + }) + + t.Run("response", func(t *testing.T) { + upstream := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + _, _ = io.WriteString(writer, "12345") + })) + defer upstream.Close() + proxy := testForwardProxy(t, upstream, 1024, 4) + request := httptest.NewRequest(http.MethodGet, "https://github.com/download", nil) + request.Header.Set("Proxy-Authorization", proxyAuthorization()) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusBadGateway { + t.Fatalf("status = %d, want %d", response.Code, http.StatusBadGateway) + } + }) +} + +func TestSCMEgressProxyRejectsPlainHTTPAndNon443Targets(t *testing.T) { + proxy := newTestSCMProxy(t, proxyConfig{AllowedHosts: map[string]struct{}{"github.com": {}}}) + + plainRequest := httptest.NewRequest(http.MethodGet, "http://github.com/repository", nil) + plainRequest.Header.Set("Proxy-Authorization", proxyAuthorization()) + plainResponse := httptest.NewRecorder() + proxy.ServeHTTP(plainResponse, plainRequest) + if plainResponse.Code != http.StatusForbidden { + t.Fatalf("plain HTTP status = %d, want %d", plainResponse.Code, http.StatusForbidden) + } + + connectResponse := httptest.NewRecorder() + proxy.ServeHTTP(connectResponse, connectRequest("github.com:8443")) + if connectResponse.Code != http.StatusForbidden { + t.Fatalf("non-443 CONNECT status = %d, want %d", connectResponse.Code, http.StatusForbidden) + } +} + +func TestSCMEgressProxyRejectsConnectedPeerMismatch(t *testing.T) { + listener, localAddress := startEchoServer(t) + defer func() { _ = listener.Close() }() + resolved := netip.MustParseAddr("1.1.1.1") + mismatched := netip.MustParseAddr("8.8.8.8") + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + Resolver: resolverFunc(func(context.Context, string, string) ([]netip.Addr, error) { + return []netip.Addr{resolved}, nil + }), + Dialer: dialerFunc(func(ctx context.Context, network, address string) (net.Conn, error) { + if network != "tcp" || address != net.JoinHostPort(resolved.String(), "443") { + t.Fatalf("dial = %s %s", network, address) + } + dialer := net.Dialer{} + connection, err := dialer.DialContext(ctx, "tcp", localAddress) + if err != nil { + return nil, err + } + return &remoteAddressConn{ + Conn: connection, + remote: &net.TCPAddr{ + IP: net.ParseIP(mismatched.String()), Port: 443, + }, + }, nil + }), + }) + response := httptest.NewRecorder() + proxy.ServeHTTP(response, connectRequest("github.com:443")) + if response.Code != http.StatusForbidden { + t.Fatalf("status = %d, want %d", response.Code, http.StatusForbidden) + } +} + +func TestSCMEgressProxyRequiresAuthentication(t *testing.T) { + proxy := newTestSCMProxy(t, proxyConfig{AllowedHosts: map[string]struct{}{"github.com": {}}}) + request := connectRequest("github.com:443") + request.Header.Del("Proxy-Authorization") + response := httptest.NewRecorder() + proxy.ServeHTTP(response, request) + if response.Code != http.StatusProxyAuthRequired { + t.Fatalf("status = %d, want %d", response.Code, http.StatusProxyAuthRequired) + } +} + +func TestAllowedHostsRequireExactLowercaseDNSNames(t *testing.T) { + for _, value := range []string{"GitHub.com", "github.com.", "127.0.0.1", "*.github.com", "github"} { + t.Run(value, func(t *testing.T) { + if _, err := allowedHosts(value, ""); err == nil { + t.Fatal("allowedHosts accepted unsafe host") + } + }) + } + hosts, err := allowedHosts("github.com", "https://api.github.com") + if err != nil { + t.Fatalf("allowedHosts: %v", err) + } + for _, host := range []string{"github.com", "api.github.com"} { + if _, ok := hosts[host]; !ok { + t.Fatalf("host %q was not allowed", host) + } + } +} + +func newTestSCMProxy(t *testing.T, config proxyConfig) *scmEgressProxy { + t.Helper() + authenticator, err := newProxyAuthenticator([]byte(testProxyToken)) + if err != nil { + t.Fatalf("new authenticator: %v", err) + } + proxy, err := newSCMEgressProxy(config, authenticator) + if err != nil { + t.Fatalf("new proxy: %v", err) + } + return proxy +} + +func connectRequest(target string) *http.Request { + request := httptest.NewRequest(http.MethodConnect, "http://proxy.invalid", nil) + request.Host = target + request.Header.Set("Proxy-Authorization", proxyAuthorization()) + return request +} + +func proxyAuthorization() string { + return "Basic " + base64.StdEncoding.EncodeToString([]byte(proxyUsername+":"+testProxyToken)) +} + +type resolverFunc func(context.Context, string, string) ([]netip.Addr, error) + +func (function resolverFunc) LookupNetIP( + ctx context.Context, + network string, + host string, +) ([]netip.Addr, error) { + return function(ctx, network, host) +} + +type dialerFunc func(context.Context, string, string) (net.Conn, error) + +func (function dialerFunc) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return function(ctx, network, address) +} + +type remoteAddressConn struct { + net.Conn + remote net.Addr +} + +func (connection *remoteAddressConn) RemoteAddr() net.Addr { return connection.remote } + +func localTestDialer(t *testing.T, localAddress string, publicAddress netip.Addr) dialerFunc { + t.Helper() + return func(ctx context.Context, network, address string) (net.Conn, error) { + if network != "tcp" || address != net.JoinHostPort(publicAddress.String(), "443") { + t.Fatalf("dial = %s %s", network, address) + } + dialer := net.Dialer{} + connection, err := dialer.DialContext(ctx, "tcp", localAddress) + if err != nil { + return nil, err + } + return &remoteAddressConn{ + Conn: connection, + remote: &net.TCPAddr{ + IP: net.ParseIP(publicAddress.String()), + Port: 443, + }, + }, nil + } +} + +func testForwardProxy( + t *testing.T, + upstream *httptest.Server, + maxRequestBytes int64, + maxResponseBytes int64, +) *scmEgressProxy { + t.Helper() + upstreamAddress := strings.TrimPrefix(upstream.URL, "https://") + publicAddress := netip.MustParseAddr("1.1.1.1") + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + MaxRequestBytes: maxRequestBytes, + MaxResponseBytes: maxResponseBytes, + Resolver: resolverFunc(func(context.Context, string, string) ([]netip.Addr, error) { + return []netip.Addr{publicAddress}, nil + }), + Dialer: localTestDialer(t, upstreamAddress, publicAddress), + }) + proxy.client = proxy.newForwardClient(&tls.Config{InsecureSkipVerify: true}) //nolint:gosec // test-only TLS server + return proxy +} + +func startEchoServer(t *testing.T) (net.Listener, string) { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen upstream: %v", err) + } + go func() { + connection, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer func() { _ = connection.Close() }() + _, _ = io.Copy(connection, connection) + }() + return listener, listener.Addr().String() +} diff --git a/cmd/orka-workspace-agent/main_test.go b/cmd/orka-workspace-agent/main_test.go index fccee07e0..0cd00b9f4 100644 --- a/cmd/orka-workspace-agent/main_test.go +++ b/cmd/orka-workspace-agent/main_test.go @@ -1880,7 +1880,22 @@ func TestWorkspaceAgentBoundsRetainedOperationResults(t *testing.T) { ); !errors.Is(err, errOperationResultExpired) { t.Fatalf("expired operation retry error = %v, want %v", err, errOperationResultExpired) } - newRequest := execRequest{OperationID: "after-result-expiry", Command: []string{"true"}} + releasePath := filepath.Join(t.TempDir(), "release-after-tombstone-eviction") + t.Cleanup(func() { + _ = os.WriteFile(releasePath, nil, 0o600) + server.mu.Lock() + cancel := server.executionCancels["after-result-expiry"] + server.mu.Unlock() + if cancel != nil { + cancel() + } + }) + newRequest := execRequest{ + OperationID: "after-result-expiry", + Command: []string{ + "sh", "-c", `while [ ! -e "$1" ]; do sleep 0.01; done`, "sh", releasePath, + }, + } if _, err := server.startExecution(newRequest, normalized, 1); err != nil { t.Fatalf("new operation rejected by tombstones: %v", err) } @@ -1893,6 +1908,29 @@ func TestWorkspaceAgentBoundsRetainedOperationResults(t *testing.T) { if remainingTombstones != 0 { t.Fatalf("expired tombstones retained = %d", remainingTombstones) } + if err := os.WriteFile(releasePath, nil, 0o600); err != nil { + t.Fatalf("release operation after tombstone eviction: %v", err) + } + deadline := time.Now().Add(3 * time.Second) + for { + result, found, conflict, expired := server.loadExecution(newRequest.OperationID, 1) + if conflict || expired || !found { + t.Fatalf( + "released operation unavailable: found=%t conflict=%t expired=%t", + found, conflict, expired, + ) + } + if !result.Running { + if result.State != workspaceagent.OperationStateSucceeded { + t.Fatalf("released operation state = %q, want %q", result.State, workspaceagent.OperationStateSucceeded) + } + break + } + if time.Now().After(deadline) { + t.Fatal("released operation did not complete") + } + time.Sleep(10 * time.Millisecond) + } if _, err := server.startExecution(request, normalized, 1); !errors.Is(err, errOperationResultExpired) { t.Fatalf("operation ownership expired within active epoch: %v", err) } diff --git a/cmd/orka-workspace-publisher/main.go b/cmd/orka-workspace-publisher/main.go new file mode 100644 index 000000000..7eb8bb94c --- /dev/null +++ b/cmd/orka-workspace-publisher/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "errors" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + publisherservice "github.com/orka-agents/orka/internal/publisher/service" +) + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) + setPrivateUmask() + config, err := publisherservice.LoadConfigFromEnv() + if err != nil { + logger.Error("invalid workspace publisher configuration", "error", err) + os.Exit(1) + } + server, err := publisherservice.New(config) + if err != nil { + logger.Error("create workspace publisher", "error", err) + os.Exit(1) + } + httpServer := &http.Server{ + Addr: config.ListenAddress, Handler: server.Handler(), + ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 3 * time.Minute, + WriteTimeout: 3 * time.Minute, IdleTimeout: 2 * time.Minute, MaxHeaderBytes: 32 << 10, + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + defer stop() + go func() { + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := httpServer.Shutdown(shutdownCtx); err != nil { + logger.Error("workspace publisher shutdown failed", "error", err) + } + }() + server.LogStartup(logger) + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error("workspace publisher failed", "error", err) + os.Exit(1) + } +} diff --git a/cmd/orka-workspace-publisher/umask_other.go b/cmd/orka-workspace-publisher/umask_other.go new file mode 100644 index 000000000..af1ae8d66 --- /dev/null +++ b/cmd/orka-workspace-publisher/umask_other.go @@ -0,0 +1,5 @@ +//go:build !unix + +package main + +func setPrivateUmask() {} diff --git a/cmd/orka-workspace-publisher/umask_unix.go b/cmd/orka-workspace-publisher/umask_unix.go new file mode 100644 index 000000000..51259812c --- /dev/null +++ b/cmd/orka-workspace-publisher/umask_unix.go @@ -0,0 +1,9 @@ +//go:build unix + +package main + +import "syscall" + +func setPrivateUmask() { + _ = syscall.Umask(0o077) +} diff --git a/config/acp-production/README.md b/config/acp-production/README.md new file mode 100644 index 000000000..af7fd6ab0 --- /dev/null +++ b/config/acp-production/README.md @@ -0,0 +1,17 @@ +# ACP production overlay + +This is the canonical direct-Kustomize deployment surface. It includes the +cross-namespace Vekil ingress policy and renders controller, provider proxy, +SCM proxy, and Workspace/Publisher images by immutable digest. + +The checked-in all-zero digests are intentional fail-closed placeholders. Use +`make deploy` with digest-pinned `IMG`, `WORKSPACE_PUBLISHER_IMG`, +`ACP_CODEX_RUNTIME_IMG`, `ACP_CLAUDE_RUNTIME_IMG`, and +`ACP_COPILOT_RUNTIME_IMG`, or replace all three runtime entries in +`runtime-images.env` before applying. Never deploy a rendered all-zero +placeholder. + +The production overlay intentionally excludes CRDs. Before the first workload +deployment, run `scripts/upgrade-orka-crds.sh` with verified backup markers and +resolve every reported v1/legacy blocker. `make deploy` verifies that cutover +state before applying only workload resources. diff --git a/config/acp-production/kustomization.yaml b/config/acp-production/kustomization.yaml new file mode 100644 index 000000000..854f03736 --- /dev/null +++ b/config/acp-production/kustomization.yaml @@ -0,0 +1,25 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# Canonical production ACP overlay. The nested ACP workload base applies the +# orka-system namespace, while the Vekil ingress policy deliberately retains +# its explicit vekil-system namespace. +resources: + - ../acp-workload + - ../vekil-ingress +configMapGenerator: + - name: acp-runtime-images + namespace: orka-system + envs: + - runtime-images.env +generatorOptions: + labels: + orka.ai/acp-runtime-images: "true" +# Fail-closed placeholders: direct rendering is immutable but not runnable +# until an operator replaces all three runtime digests (make deploy does this atomically). +images: + - name: ghcr.io/orka-agents/orka + newName: docker.io/sozercan/orka + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + - name: docker.io/sozercan/orka-workspace-publisher + newName: docker.io/sozercan/orka-workspace-publisher + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 diff --git a/config/acp-production/runtime-images.env b/config/acp-production/runtime-images.env new file mode 100644 index 000000000..df070c7c0 --- /dev/null +++ b/config/acp-production/runtime-images.env @@ -0,0 +1,6 @@ +# Fail-closed placeholders. `make deploy` renders a temporary overlay with +# immutable runtime references; Kustomize hashes this ConfigMap generation and +# rewrites the controller's configMapKeyRef so every change rolls the Pod. +ORKA_ACP_CODEX_RUNTIME_IMAGE=docker.io/sozercan/orka-acp-codex@sha256:0000000000000000000000000000000000000000000000000000000000000000 +ORKA_ACP_CLAUDE_RUNTIME_IMAGE=docker.io/sozercan/orka-acp-claude@sha256:0000000000000000000000000000000000000000000000000000000000000000 +ORKA_ACP_COPILOT_RUNTIME_IMAGE=docker.io/sozercan/orka-acp-copilot@sha256:0000000000000000000000000000000000000000000000000000000000000000 diff --git a/config/acp-workload/README.md b/config/acp-workload/README.md new file mode 100644 index 000000000..55fbec697 --- /dev/null +++ b/config/acp-workload/README.md @@ -0,0 +1,16 @@ +# ACP workload base + +This Kustomize base deploys the Orka controller, worker RBAC, ACP runtime broker +proxies, and the clean-room workspace publisher without installing CRDs. + +Before applying it directly, create the required Secrets in `orka-system`: + +- `acp-artifact-capability` with `capability-secret` +- `workspace-publisher-auth` with `controller-token` and `operation-capability-secret` +- `provider-auth-proxy` with `token` +- `scm-egress-proxy-auth` with `token` + +Use `config/acp-production` or `make deploy` for the supported digest-pinned +production flow. That path creates missing Secrets without printing their values, +renders immutable controller, runtime, and publisher image references, and applies +the namespace and generated runtime-image ConfigMap before dependent workloads. diff --git a/config/harness-wrapper/service.yaml b/config/acp-workload/api_service.yaml similarity index 57% rename from config/harness-wrapper/service.yaml rename to config/acp-workload/api_service.yaml index d79491e77..e6cb85bd9 100644 --- a/config/harness-wrapper/service.yaml +++ b/config/acp-workload/api_service.yaml @@ -1,16 +1,18 @@ apiVersion: v1 kind: Service metadata: - name: agent-harness-wrapper labels: + control-plane: controller-manager app.kubernetes.io/name: orka - app.kubernetes.io/component: agent-harness-wrapper app.kubernetes.io/managed-by: kustomize + name: api + namespace: system spec: - selector: - app.kubernetes.io/name: orka - app.kubernetes.io/component: agent-harness-wrapper ports: - name: http port: 8080 - targetPort: http + protocol: TCP + targetPort: 8080 + selector: + control-plane: controller-manager + app.kubernetes.io/name: orka diff --git a/config/acp-workload/kustomization.yaml b/config/acp-workload/kustomization.yaml new file mode 100644 index 000000000..e784e0ed1 --- /dev/null +++ b/config/acp-workload/kustomization.yaml @@ -0,0 +1,111 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: orka-system +namePrefix: orka- +configurations: + - kustomizeconfig.yaml +resources: + - ../rbac + - ../manager + - ../publisher + - ../provider-proxy + - ../scm-egress-proxy + - ../admission + - ../policy + - metrics_service.yaml + - api_service.yaml +patches: + - path: manager_metrics_patch.yaml + target: + kind: Deployment + name: controller-manager +replacements: +- source: + group: "" + version: v1 + kind: Namespace + name: system + fieldPath: metadata.name + targets: + - select: + group: admissionregistration.k8s.io + version: v1 + kind: ValidatingAdmissionPolicy + name: gateway-task-protection + fieldPaths: + - spec.variables.[name=controllerNamespace].expression + options: + delimiter: "'" + index: 1 +- source: + group: "" + version: v1 + kind: ServiceAccount + name: controller-manager + fieldPath: metadata.name + targets: + - select: + group: admissionregistration.k8s.io + version: v1 + kind: ValidatingAdmissionPolicy + name: gateway-task-protection + fieldPaths: + - spec.variables.[name=controllerServiceAccount].expression + options: + delimiter: "'" + index: 1 +- source: + group: "" + version: v1 + kind: ServiceAccount + name: ai-worker + fieldPath: metadata.name + targets: + - select: + group: admissionregistration.k8s.io + version: v1 + kind: ValidatingAdmissionPolicy + name: gateway-task-protection + fieldPaths: + - spec.variables.[name=aiWorkerServiceAccount].expression + options: + delimiter: "'" + index: 1 +- source: + group: "" + version: v1 + kind: ServiceAccount + name: vendor-worker + fieldPath: metadata.name + targets: + - select: + group: admissionregistration.k8s.io + version: v1 + kind: ValidatingAdmissionPolicy + name: gateway-task-protection + fieldPaths: + - spec.variables.[name=vendorWorkerServiceAccount].expression + options: + delimiter: "'" + index: 1 +- source: + group: "" + version: v1 + kind: ServiceAccount + name: container-worker + fieldPath: metadata.name + targets: + - select: + group: admissionregistration.k8s.io + version: v1 + kind: ValidatingAdmissionPolicy + name: gateway-task-protection + fieldPaths: + - spec.variables.[name=containerWorkerServiceAccount].expression + options: + delimiter: "'" + index: 1 + +transformers: +- remove_admission_policy_namespace.yaml +- remove_admission_policy_binding_namespace.yaml diff --git a/config/acp-workload/kustomizeconfig.yaml b/config/acp-workload/kustomizeconfig.yaml new file mode 100644 index 000000000..0e70f1858 --- /dev/null +++ b/config/acp-workload/kustomizeconfig.yaml @@ -0,0 +1,9 @@ +nameReference: +- kind: ClusterRole + group: rbac.authorization.k8s.io + version: v1 + fieldSpecs: + - kind: ClusterRole + group: rbac.authorization.k8s.io + version: v1 + path: rules/resourceNames diff --git a/config/acp-workload/manager_metrics_patch.yaml b/config/acp-workload/manager_metrics_patch.yaml new file mode 100644 index 000000000..2aaef6536 --- /dev/null +++ b/config/acp-workload/manager_metrics_patch.yaml @@ -0,0 +1,4 @@ +# This patch adds the args to allow exposing the metrics endpoint using HTTPS +- op: add + path: /spec/template/spec/containers/0/args/0 + value: --metrics-bind-address=:8443 diff --git a/config/acp-workload/metrics_service.yaml b/config/acp-workload/metrics_service.yaml new file mode 100644 index 000000000..dfb4408f6 --- /dev/null +++ b/config/acp-workload/metrics_service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: orka + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: controller-manager + app.kubernetes.io/name: orka diff --git a/config/acp-workload/remove_admission_policy_binding_namespace.yaml b/config/acp-workload/remove_admission_policy_binding_namespace.yaml new file mode 100644 index 000000000..c5c2ab765 --- /dev/null +++ b/config/acp-workload/remove_admission_policy_binding_namespace.yaml @@ -0,0 +1,12 @@ +apiVersion: builtin +kind: PatchTransformer +metadata: + name: remove-admission-policy-binding-namespace +patch: |- + - op: remove + path: /metadata/namespace +target: + group: admissionregistration.k8s.io + version: v1 + kind: ValidatingAdmissionPolicyBinding + name: gateway-task-protection diff --git a/config/acp-workload/remove_admission_policy_namespace.yaml b/config/acp-workload/remove_admission_policy_namespace.yaml new file mode 100644 index 000000000..a510dab5a --- /dev/null +++ b/config/acp-workload/remove_admission_policy_namespace.yaml @@ -0,0 +1,12 @@ +apiVersion: builtin +kind: PatchTransformer +metadata: + name: remove-admission-policy-namespace +patch: |- + - op: remove + path: /metadata/namespace +target: + group: admissionregistration.k8s.io + version: v1 + kind: ValidatingAdmissionPolicy + name: gateway-task-protection diff --git a/config/crd/bases/core.orka.ai_agentruntimes.yaml b/config/crd/bases/core.orka.ai_agentruntimes.yaml index e9623b7ae..5c4b43c6d 100644 --- a/config/crd/bases/core.orka.ai_agentruntimes.yaml +++ b/config/crd/bases/core.orka.ai_agentruntimes.yaml @@ -24,8 +24,8 @@ spec: - jsonPath: .spec.deployment.mode name: Mode type: string - - jsonPath: .status.observedCapabilities.runtimeName - name: Runtime + - jsonPath: .status.observedCapabilities.runtimeInstanceID + name: Instance type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -33,7 +33,8 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: AgentRuntime is the Schema for registered Orka harness runtimes. + description: AgentRuntime is the Schema for registered external Orka harness + runtimes. properties: apiVersion: description: |- @@ -57,96 +58,306 @@ spec: Orka harness runtime. properties: capabilities: - description: Capabilities declares readiness requirements Orka checks - against the runtime. + description: Capabilities pins the exact instance, profile, limits, + and governance claims. properties: - brokeredToolClasses: - description: BrokeredToolClasses lists brokered tool classes the - runtime must advertise when brokered mode is required. - items: - description: AgentRuntimeBrokeredToolClass declares which classes - of Orka-brokered tools a runtime can request. - enum: - - read - - write - - coordination - type: string - type: array - x-kubernetes-list-type: set - supportsArtifacts: - description: SupportsArtifacts requires the runtime to advertise - artifact/result reference support when true. - type: boolean - supportsCancel: - description: SupportsCancel requires the runtime to advertise - cancellation support when true. - type: boolean - supportsContinuation: - description: SupportsContinuation requires the runtime to advertise - continuation after Orka-brokered tool results when true. + limits: + description: Limits must exactly match /v2/capabilities. + properties: + maxBufferedEvents: + format: int32 + minimum: 1 + type: integer + maxConcurrentPrompts: + format: int32 + minimum: 1 + type: integer + maxEventLineBytes: + format: int32 + minimum: 1 + type: integer + maxPendingPermissions: + format: int32 + minimum: 1 + type: integer + maxPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + maxRequestBytes: + format: int32 + minimum: 1 + type: integer + maxResidentSessions: + format: int32 + minimum: 1 + type: integer + maxTerminalResultBytes: + format: int32 + minimum: 1 + type: integer + maxUpdateEventsPerSecond: + format: int32 + minimum: 1 + type: integer + maxWorkspaceDeltaBytes: + format: int64 + minimum: 1 + type: integer + minPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + required: + - maxBufferedEvents + - maxConcurrentPrompts + - maxEventLineBytes + - maxPendingPermissions + - maxPromptLeaseMillis + - maxRequestBytes + - maxResidentSessions + - maxTerminalResultBytes + - maxUpdateEventsPerSecond + - maxWorkspaceDeltaBytes + - minPromptLeaseMillis + type: object + profile: + description: Profile is the exact immutable profile accepted by + session creation. + properties: + acpProfile: + description: ACPProfile is the reviewed ACP profile. + enum: + - acp.v1 + type: string + adapterDigest: + description: AdapterDigest pins the adapter/CLI artifact set. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + adapterName: + description: AdapterName identifies the sole adapter contained + by this external profile. + maxLength: 128 + minLength: 1 + type: string + agentConfigurationDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + approvalPolicyDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digest: + description: Digest is the canonical orka.harness.v2 runtime-profile + digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digestSchemaVersion: + description: DigestSchemaVersion identifies the canonical + profile digest schema. + enum: + - 1 + format: int32 + type: integer + mcpConfigurationDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + model: + maxLength: 256 + minLength: 1 + type: string + providerKind: + maxLength: 128 + minLength: 1 + type: string + proxyCredentialRole: + maxLength: 256 + minLength: 1 + type: string + proxyCredentialScope: + maxLength: 1024 + minLength: 1 + type: string + resourceClass: + maxLength: 128 + minLength: 1 + type: string + toolPolicyDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + workspaceIntent: + description: WorkspaceIntent is the one immutable strict intent + represented by this profile. + enum: + - read + - write + type: string + required: + - acpProfile + - adapterDigest + - adapterName + - agentConfigurationDigest + - approvalPolicyDigest + - digest + - digestSchemaVersion + - mcpConfigurationDigest + - model + - providerKind + - proxyCredentialRole + - proxyCredentialScope + - resourceClass + - toolPolicyDigest + - workspaceIntent + type: object + runtimeInstanceID: + description: |- + RuntimeInstanceID is the immutable external supervisor instance expected from + authenticated /v2/status and every conformance response. + maxLength: 253 + minLength: 1 + type: string + supportsDrain: + description: SupportsDrain must exactly match the static capability + claim. type: boolean - supportsRuntimeSessions: - description: SupportsRuntimeSessions requires the runtime to advertise - stable runtime sessions when true. + supportsPublicationFinalization: + description: SupportsPublicationFinalization must exactly match + the static capability claim. type: boolean - toolExecutionModes: - description: ToolExecutionModes lists tool execution modes the - runtime must advertise. - items: - description: AgentRuntimeToolExecutionMode declares how custom - runtimes interact with tools. - enum: - - observed - - brokered - type: string - type: array - x-kubernetes-list-type: set + workspaceGovernance: + description: WorkspaceGovernance must exactly match the static + capability claim. + properties: + cancellationSettlement: + type: boolean + duplicateSafeMutations: + type: boolean + exactInstanceFencing: + type: boolean + mode: + description: Mode selects strict Orka governance or an explicit + trusted escape hatch. + enum: + - strict-governed + - trusted-non-governed + type: string + noDirectSCMPublication: + type: boolean + orkaOwnedCleanRoomPublication: + type: boolean + orkaOwnedWorkspaceDeltas: + type: boolean + promptScopedBrokerAuthorization: + type: boolean + trusted: + description: |- + Trusted must be true only for trusted-non-governed runtimes. Such runtimes + are ineligible for Tasks requesting strict read or write guarantees. + type: boolean + required: + - cancellationSettlement + - duplicateSafeMutations + - exactInstanceFencing + - mode + - noDirectSCMPublication + - orkaOwnedCleanRoomPublication + - orkaOwnedWorkspaceDeltas + - promptScopedBrokerAuthorization + - trusted + type: object + x-kubernetes-validations: + - message: trusted-non-governed runtimes must be explicitly marked + trusted + rule: self.mode != 'trusted-non-governed' || self.trusted + - message: strict-governed runtimes must not use the trusted non-governed + escape hatch + rule: self.mode != 'strict-governed' || !self.trusted + - message: strict-governed runtimes must claim every strict workspace + governance guarantee + rule: self.mode != 'strict-governed' || (self.orkaOwnedWorkspaceDeltas + && self.promptScopedBrokerAuthorization && self.noDirectSCMPublication + && self.orkaOwnedCleanRoomPublication && self.exactInstanceFencing + && self.duplicateSafeMutations && self.cancellationSettlement) + - message: trusted-non-governed runtimes must not claim strict + workspace guarantees + rule: self.mode != 'trusted-non-governed' || (!self.orkaOwnedWorkspaceDeltas + && !self.promptScopedBrokerAuthorization && !self.noDirectSCMPublication + && !self.orkaOwnedCleanRoomPublication && !self.exactInstanceFencing + && !self.duplicateSafeMutations && !self.cancellationSettlement) + required: + - limits + - profile + - runtimeInstanceID + - supportsDrain + - workspaceGovernance type: object clientAuth: - description: ClientAuth configures controller-to-runtime authentication. + description: ClientAuth configures controller authentication and mutation + authorization. properties: - bearerTokenSecretRef: + controllerBearerTokenSecretRef: + description: |- + ControllerBearerTokenSecretRef supplies the controller bearer token used by + authenticated v2 status and mutation endpoints. + properties: + key: + description: Key is the Secret data key. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is the Secret name in the AgentRuntime namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + operationCapabilitySecretRef: description: |- - BearerAuthRef points to the bearer token Secret used for mutating harness endpoints. - The referenced Secret must opt in with label orka.ai/agent-runtime-auth=true, - may set orka.ai/agent-runtime-name= to restrict use to one AgentRuntime, - and must set annotation orka.ai/agent-runtime-endpoint= to bind the token to one endpoint. + OperationCapabilitySecretRef supplies the HMAC secret used to bind every + mutation to its exact fence, operation identity, request digest, and expiry. properties: key: - description: Key is the Secret data key containing the bearer - token. + description: Key is the Secret data key. + maxLength: 253 minLength: 1 type: string name: - description: Name is the Secret name. + description: Name is the Secret name in the AgentRuntime namespace. + maxLength: 253 minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - key - name type: object required: - - bearerTokenSecretRef + - controllerBearerTokenSecretRef + - operationCapabilitySecretRef type: object contractVersion: - default: orka.harness.v1 + default: orka.harness.v2 description: ContractVersion is the Orka harness contract this runtime must implement. enum: - - orka.harness.v1 + - orka.harness.v2 type: string deployment: description: Deployment identifies the runtime endpoint provider. properties: endpoint: description: |- - Endpoint is the base URL for a pre-deployed or external orka.harness.v1 service. - It must not contain credentials; bearer auth is configured via clientAuth. + Endpoint is the base URL for an external orka.harness.v2 service. It must + not contain credentials, query parameters, or fragments. pattern: ^https?://[^\s@?#]+$ type: string mode: - description: Mode is the deployment mode. The first milestone - supports external endpoints only. + description: |- + Mode is the deployment mode. External AgentRuntime registrations are not + scaled or recycled by Orka. enum: - external-endpoint type: string @@ -155,6 +366,7 @@ spec: - mode type: object required: + - capabilities - clientAuth - contractVersion - deployment @@ -230,101 +442,185 @@ spec: message: description: Message provides sanitized readiness context. type: string - observedAuthRefResourceVersion: - description: |- - ObservedAuthRefResourceVersion is the resourceVersion of the bearer auth Secret - used for the last readiness probe. It is non-secret metadata used to decide - when token rotation requires a fresh authenticated conformance turn. - type: string observedCapabilities: description: ObservedCapabilities contains sanitized capabilities from the last probe. properties: - brokeredToolClasses: - description: BrokeredToolClasses are the brokered tool classes - advertised by /v1/capabilities. - items: - description: AgentRuntimeBrokeredToolClass declares which classes - of Orka-brokered tools a runtime can request. - enum: - - read - - write - - coordination - type: string - type: array - x-kubernetes-list-type: set - maxConcurrentTurns: - description: MaxConcurrentTurns is the advertised concurrency - ceiling. - type: integer - maxOutputBytes: - description: MaxOutputBytes is the advertised maximum output payload - size. + acpVersion: + type: string + adapterDigest: + type: string + adapterName: + type: string + controllerEpoch: format: int64 type: integer - maxTurnSeconds: - description: MaxTurnSeconds is the advertised per-turn duration - ceiling. + lifecycle: + type: string + limits: + description: AgentRuntimeProtocolLimits pins the exact bounded + v2 limits expected from the runtime. + properties: + maxBufferedEvents: + format: int32 + minimum: 1 + type: integer + maxConcurrentPrompts: + format: int32 + minimum: 1 + type: integer + maxEventLineBytes: + format: int32 + minimum: 1 + type: integer + maxPendingPermissions: + format: int32 + minimum: 1 + type: integer + maxPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + maxRequestBytes: + format: int32 + minimum: 1 + type: integer + maxResidentSessions: + format: int32 + minimum: 1 + type: integer + maxTerminalResultBytes: + format: int32 + minimum: 1 + type: integer + maxUpdateEventsPerSecond: + format: int32 + minimum: 1 + type: integer + maxWorkspaceDeltaBytes: + format: int64 + minimum: 1 + type: integer + minPromptLeaseMillis: + format: int64 + minimum: 1 + type: integer + required: + - maxBufferedEvents + - maxConcurrentPrompts + - maxEventLineBytes + - maxPendingPermissions + - maxPromptLeaseMillis + - maxRequestBytes + - maxResidentSessions + - maxTerminalResultBytes + - maxUpdateEventsPerSecond + - maxWorkspaceDeltaBytes + - minPromptLeaseMillis + type: object + model: + type: string + profileDigestSchemaVersion: + format: int32 type: integer protocolVersion: - description: ProtocolVersion is the runtime's advertised Orka - protocol version. type: string providerKind: - description: ProviderKind is the provider kind advertised by /v1/capabilities. type: string - runtimeName: - description: RuntimeName is the runtime name advertised by /v1/capabilities. + runtimeInstanceID: type: string - runtimeVersion: - description: RuntimeVersion is the runtime version advertised - by /v1/capabilities. + runtimePoolGeneration: + format: int64 + type: integer + runtimePoolUID: type: string - supportsArtifacts: - description: SupportsArtifacts reports whether the runtime advertises - artifact/result reference support. - type: boolean - supportsCancel: - description: SupportsCancel reports whether the runtime advertises - cancellation support. - type: boolean - supportsContinuation: - description: SupportsContinuation reports whether the runtime - advertises continuation support. - type: boolean - supportsRuntimeSessions: - description: SupportsRuntimeSessions reports whether the runtime - advertises runtime-session support. - type: boolean - supportsSuspend: - description: SupportsSuspend reports whether the runtime advertises - suspend support. + runtimeProfileDigest: + type: string + supervisorBootID: + type: string + supportsDrain: type: boolean - supportsWorkspaceSnapshot: - description: SupportsWorkspaceSnapshot reports whether the runtime - advertises workspace snapshots. + supportsPublicationFinalization: type: boolean - toolExecutionModes: - description: ToolExecutionModes are the tool modes advertised - by /v1/capabilities. - items: - description: AgentRuntimeToolExecutionMode declares how custom - runtimes interact with tools. - enum: - - observed - - brokered - type: string - type: array - x-kubernetes-list-type: set transport: - description: Transport is the runtime transport, normally http+sse. type: string + workspaceGovernance: + description: |- + AgentRuntimeWorkspaceGovernanceCapabilities are static claims advertised by + /v2/capabilities and exercised by the hostile conformance cycle. + properties: + cancellationSettlement: + type: boolean + duplicateSafeMutations: + type: boolean + exactInstanceFencing: + type: boolean + mode: + description: Mode selects strict Orka governance or an explicit + trusted escape hatch. + enum: + - strict-governed + - trusted-non-governed + type: string + noDirectSCMPublication: + type: boolean + orkaOwnedCleanRoomPublication: + type: boolean + orkaOwnedWorkspaceDeltas: + type: boolean + promptScopedBrokerAuthorization: + type: boolean + trusted: + description: |- + Trusted must be true only for trusted-non-governed runtimes. Such runtimes + are ineligible for Tasks requesting strict read or write guarantees. + type: boolean + required: + - cancellationSettlement + - duplicateSafeMutations + - exactInstanceFencing + - mode + - noDirectSCMPublication + - orkaOwnedCleanRoomPublication + - orkaOwnedWorkspaceDeltas + - promptScopedBrokerAuthorization + - trusted + type: object + x-kubernetes-validations: + - message: trusted-non-governed runtimes must be explicitly marked + trusted + rule: self.mode != 'trusted-non-governed' || self.trusted + - message: strict-governed runtimes must not use the trusted non-governed + escape hatch + rule: self.mode != 'strict-governed' || !self.trusted + - message: strict-governed runtimes must claim every strict workspace + governance guarantee + rule: self.mode != 'strict-governed' || (self.orkaOwnedWorkspaceDeltas + && self.promptScopedBrokerAuthorization && self.noDirectSCMPublication + && self.orkaOwnedCleanRoomPublication && self.exactInstanceFencing + && self.duplicateSafeMutations && self.cancellationSettlement) + - message: trusted-non-governed runtimes must not claim strict + workspace guarantees + rule: self.mode != 'trusted-non-governed' || (!self.orkaOwnedWorkspaceDeltas + && !self.promptScopedBrokerAuthorization && !self.noDirectSCMPublication + && !self.orkaOwnedCleanRoomPublication && !self.exactInstanceFencing + && !self.duplicateSafeMutations && !self.cancellationSettlement) type: object + observedControllerAuthRefResourceVersion: + description: |- + ObservedControllerAuthRefResourceVersion is the bearer Secret version used + by the last successful or failed authenticated conformance probe. + type: string observedGeneration: description: ObservedGeneration is the latest generation reconciled into this status. format: int64 type: integer + observedOperationCapabilityRefResourceVersion: + description: |- + ObservedOperationCapabilityRefResourceVersion is the HMAC Secret version used + by the last mutation conformance probe. + type: string ready: description: Ready indicates the runtime passed the configured Orka readiness checks. diff --git a/config/crd/bases/core.orka.ai_agents.yaml b/config/crd/bases/core.orka.ai_agents.yaml index 6bcb07722..04774ed61 100644 --- a/config/crd/bases/core.orka.ai_agents.yaml +++ b/config/crd/bases/core.orka.ai_agents.yaml @@ -1094,10 +1094,10 @@ spec: type: array workspace: description: |- - Workspace requests an upstream agent-sandbox execution workspace for agent Tasks. - When enabled, the Task controller validates the request and propagates the - resolved sandbox settings to the agent worker Job. The worker wrapper then - claims the sandbox workspace and runs the configured agent runtime inside it. + Workspace requests an execution workspace for worker-backed Task types. + ACP core agent Tasks reject this field because their ephemeral workspace is + owned by RuntimeSession lifecycle and clean-room publication. Actor-backed + RuntimeSession support is a future integration behind the v2 lifecycle seam. properties: boot: description: |- @@ -1284,7 +1284,6 @@ spec: - openai type: string temperature: - default: 0.7 description: Temperature controls randomness in generation maximum: 2 minimum: 0 @@ -1428,10 +1427,9 @@ spec: description: Type specifies which built-in CLI runtime to use. Use runtimeRef for admin-registered custom runtimes. enum: - - copilot - claude - codex - - opencode + - copilot type: string type: object x-kubernetes-validations: @@ -1520,6 +1518,9 @@ spec: description: Inline is the inline prompt text type: string type: object + x-kubernetes-validations: + - message: system prompt must use only one of inline or configMapRef + rule: '!(has(self.inline) && self.inline.size() > 0 && has(self.configMapRef))' tools: description: Tools lists the default tools available to this agent items: diff --git a/config/crd/bases/core.orka.ai_branchclaims.yaml b/config/crd/bases/core.orka.ai_branchclaims.yaml new file mode 100644 index 000000000..1328a8fe0 --- /dev/null +++ b/config/crd/bases/core.orka.ai_branchclaims.yaml @@ -0,0 +1,197 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: branchclaims.core.orka.ai +spec: + group: core.orka.ai + names: + kind: BranchClaim + listKind: BranchClaimList + plural: branchclaims + shortNames: + - bclaim + singular: branchclaim + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.repositoryId + name: Repository + type: string + - jsonPath: .spec.ref + name: Ref + type: string + - jsonPath: .spec.ownerKind + name: Owner + type: string + - jsonPath: .status.generation + name: Generation + type: integer + - jsonPath: .status.availability + name: Availability + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + BranchClaim is the cluster-wide Kubernetes-authoritative ownership and exact + baseline record for one canonical repository branch. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: BranchClaimSpec is the immutable repository/ref ownership + identity. + properties: + id: + maxLength: 1024 + minLength: 1 + type: string + ownerKind: + description: BranchClaimOwnerKind identifies the durable owner of + an Orka-managed branch. + enum: + - Task + - Session + type: string + ownerUid: + maxLength: 1024 + minLength: 1 + type: string + ref: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + repositoryId: + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + required: + - id + - ownerKind + - ownerUid + - ref + - repositoryId + - requestDigest + type: object + x-kubernetes-validations: + - message: branch claim spec is immutable + rule: self == oldSelf + status: + description: BranchClaimStatus is the exact generation, baseline, and + availability CAS. + properties: + availability: + description: BranchClaimAvailability gates further branch mutation. + enum: + - Available + - ReconciliationBlocked + type: string + blockedReason: + maxLength: 16384 + type: string + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + generation: + format: int64 + minimum: 1 + type: integer + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + lastVerified: + description: LastVerified is the independently observed exact target + ref. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + relatedPublicationId: + maxLength: 1024 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: available branch claims must clear block metadata + rule: '!has(self.availability) || self.availability != ''Available'' + || ((!has(self.blockedReason) || size(self.blockedReason) == 0) && + (!has(self.relatedPublicationId) || size(self.relatedPublicationId) + == 0))' + - message: reconciliation-blocked branch claims require a reason + rule: '!has(self.availability) || self.availability != ''ReconciliationBlocked'' + || (has(self.blockedReason) && size(self.blockedReason) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.orka.ai_controllerepochs.yaml b/config/crd/bases/core.orka.ai_controllerepochs.yaml new file mode 100644 index 000000000..9d1c486f2 --- /dev/null +++ b/config/crd/bases/core.orka.ai_controllerepochs.yaml @@ -0,0 +1,109 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: controllerepochs.core.orka.ai +spec: + group: core.orka.ai + names: + kind: ControllerEpoch + listKind: ControllerEpochList + plural: controllerepochs + shortNames: + - cepoch + singular: controllerepoch + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.epoch + name: Epoch + type: integer + - jsonPath: .status.holderId + name: Holder + type: string + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ControllerEpoch is the human-visible Kubernetes control record paired with + an authoritative namespaced Lease. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ControllerEpochSpec is the immutable epoch-domain identity. The associated + coordination.k8s.io Lease is the CAS authority for holder and epoch changes. + properties: + name: + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: controller epoch spec is immutable + rule: self == oldSelf + status: + description: |- + ControllerEpochStatus mirrors the authoritative Lease state for inspection + and recovery. LeaseResourceVersion identifies the exact Lease revision. + properties: + acquiredAt: + format: date-time + type: string + epoch: + format: int64 + minimum: 1 + type: integer + holderId: + maxLength: 1024 + type: string + leaseName: + maxLength: 253 + type: string + leaseResourceVersion: + maxLength: 64 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + updatedAt: + format: date-time + type: string + version: + format: int64 + minimum: 1 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.orka.ai_externaleffects.yaml b/config/crd/bases/core.orka.ai_externaleffects.yaml new file mode 100644 index 000000000..e17b0a44f --- /dev/null +++ b/config/crd/bases/core.orka.ai_externaleffects.yaml @@ -0,0 +1,191 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: externaleffects.core.orka.ai +spec: + group: core.orka.ai + names: + kind: ExternalEffect + listKind: ExternalEffectList + plural: externaleffects + shortNames: + - eeffect + singular: externaleffect + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .spec.kind + name: Kind + type: string + - jsonPath: .status.attempts + name: Attempts + type: integer + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ExternalEffect is the Kubernetes-authoritative canonical idempotency record + for an operation outside SQLite. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ExternalEffectSpec is the immutable canonical identity and request binding. + The identity namespace intentionally duplicates metadata.namespace so a + serialized record remains self-describing and can be checked fail-closed. + properties: + aggregateId: + maxLength: 1024 + minLength: 1 + type: string + id: + maxLength: 1024 + minLength: 1 + type: string + identityNamespace: + maxLength: 1024 + minLength: 1 + type: string + kind: + maxLength: 1024 + minLength: 1 + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + required: + - aggregateId + - id + - identityNamespace + - kind + - operationId + - requestDigest + type: object + x-kubernetes-validations: + - message: external effect spec is immutable + rule: self == oldSelf + status: + description: |- + ExternalEffectStatus contains the mutable state, response, lease, and epoch + fence for one canonical external effect. + properties: + attempts: + format: int64 + minimum: 0 + type: integer + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + leaseExpiresAt: + format: date-time + type: string + leaseOwner: + maxLength: 1024 + type: string + response: + description: |- + Response stores a bounded JSON response for idempotent replay. Large + response bodies should remain in the artifact store and be referenced by + a compact receipt instead. + x-kubernetes-preserve-unknown-fields: true + responseDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + state: + description: |- + ExternalEffectControlState is the durable state of one idempotent operation + performed outside the controller's SQLite transaction boundary. + enum: + - Pending + - InFlight + - Succeeded + - Failed + - OutcomeUnknown + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: in-flight external effects require a lease owner and expiry + rule: '!has(self.state) || self.state != ''InFlight'' || (has(self.leaseOwner) + && size(self.leaseOwner) > 0 && has(self.leaseExpiresAt))' + - message: non-in-flight external effects must clear lease fields + rule: '!has(self.state) || self.state == ''InFlight'' || ((!has(self.leaseOwner) + || size(self.leaseOwner) == 0) && !has(self.leaseExpiresAt))' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.orka.ai_promptattempts.yaml b/config/crd/bases/core.orka.ai_promptattempts.yaml new file mode 100644 index 000000000..e9bdef997 --- /dev/null +++ b/config/crd/bases/core.orka.ai_promptattempts.yaml @@ -0,0 +1,264 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: promptattempts.core.orka.ai +spec: + group: core.orka.ai + names: + kind: PromptAttempt + listKind: PromptAttemptList + plural: promptattempts + shortNames: + - pattempt + singular: promptattempt + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.executionState + name: Execution + type: string + - jsonPath: .status.deliveryState + name: Delivery + type: string + - jsonPath: .spec.attempt + name: Attempt + type: integer + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PromptAttempt is the Kubernetes-authoritative prompt execution and delivery + control record. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PromptAttemptSpec is the immutable identity and request binding for one + Task prompt attempt. + properties: + attempt: + description: Attempt is the one-based Task attempt number. + format: int64 + minimum: 1 + type: integer + credentialBindings: + description: CredentialBindings is the immutable, role-separated Secret + identity set. + items: + description: |- + PromptCredentialBinding freezes one role-specific Secret identity without + storing credential material. + properties: + namespace: + maxLength: 253 + minLength: 1 + type: string + resourceVersion: + maxLength: 253 + minLength: 1 + type: string + role: + enum: + - SourceRead + - TargetRead + - TargetWrite + - Forge + type: string + secretKey: + maxLength: 253 + minLength: 1 + type: string + secretName: + maxLength: 253 + minLength: 1 + type: string + secretUid: + maxLength: 253 + minLength: 1 + type: string + required: + - namespace + - resourceVersion + - role + - secretKey + - secretName + - secretUid + type: object + maxItems: 4 + type: array + x-kubernetes-list-map-keys: + - role + x-kubernetes-list-type: map + id: + description: ID is the canonical DurableControlStore prompt-attempt + ID. + maxLength: 1024 + minLength: 1 + type: string + promptId: + description: PromptID is the immutable prompt identity within the + attempt. + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + description: RequestDigest binds the prompt identity to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + taskUid: + description: TaskUID is the immutable Kubernetes UID of the Task. + maxLength: 1024 + minLength: 1 + type: string + required: + - attempt + - id + - promptId + - requestDigest + - taskUid + type: object + x-kubernetes-validations: + - message: prompt attempt spec is immutable + rule: self == oldSelf + status: + description: PromptAttemptStatus holds the exact execution and delivery + state machines. + properties: + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + deliveryState: + description: PromptAttemptDeliveryState is the durable delivery state + for one prompt. + enum: + - NotRequested + - Validating + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - ReadValidated + - NoChange + - CancelledBeforePublish + - ReadOnlyWorkspaceModified + - DeliveryConflict + - CredentialBlocked + - PublicationOutcomeUnknown + type: string + executionState: + description: PromptAttemptExecutionState is the durable prompt execution + state. + enum: + - Queued + - Reserved + - SessionStarting + - Planned + - Submitting + - SubmittedUnknown + - Accepted + - Running + - Settling + - Succeeded + - Failed + - Cancelled + - OutcomeUnknown + type: string + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + outcomeMarker: + maxLength: 16384 + type: string + runtimeInstanceId: + description: RuntimeInstanceID is immutable after first binding. + maxLength: 1024 + type: string + sessionLeaseGeneration: + description: SessionLeaseGeneration is immutable after first binding. + format: int64 + minimum: 1 + type: integer + sessionUid: + description: SessionUID is immutable after first binding. + maxLength: 1024 + type: string + terminalReason: + maxLength: 16384 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: OutcomeUnknown requires an explicit outcome marker + rule: '!has(self.executionState) || self.executionState != ''OutcomeUnknown'' + || (has(self.outcomeMarker) && size(self.outcomeMarker) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.orka.ai_publications.yaml b/config/crd/bases/core.orka.ai_publications.yaml new file mode 100644 index 000000000..0b921bca9 --- /dev/null +++ b/config/crd/bases/core.orka.ai_publications.yaml @@ -0,0 +1,529 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: publications.core.orka.ai +spec: + group: core.orka.ai + names: + kind: Publication + listKind: PublicationList + plural: publications + shortNames: + - pubctl + singular: publication + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .spec.generation + name: Generation + type: integer + - jsonPath: .spec.targetRef + name: Target + type: string + - jsonPath: .status.version + name: Version + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Publication is the Kubernetes-authoritative clean-room publication + record. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PublicationSpec is the immutable clean-room publication identity and input. + Mutable receipts and forge intent live only in status. + properties: + artifactDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + artifactId: + description: ArtifactID identifies the durable content-addressed change + artifact. + maxLength: 1024 + minLength: 1 + type: string + artifactMediaType: + maxLength: 255 + minLength: 1 + type: string + artifactSizeBytes: + format: int64 + minimum: 1 + type: integer + attempt: + format: int64 + minimum: 1 + type: integer + baseline: + description: |- + ControlRemoteRefState is an exact remote-ref observation. Absent and SHA are + mutually exclusive. The all-zero value is reserved for an explicitly unknown + observation in PublicationOutcomeUnknown receipts. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + branchClaimGeneration: + format: int64 + minimum: 1 + type: integer + branchClaimId: + maxLength: 1024 + minLength: 1 + type: string + commitIdentity: + maxLength: 1024 + minLength: 1 + type: string + commitMessage: + maxLength: 16384 + minLength: 1 + type: string + commitTimestamp: + format: date-time + type: string + generation: + format: int64 + minimum: 1 + type: integer + id: + maxLength: 1024 + minLength: 1 + type: string + promptId: + maxLength: 1024 + minLength: 1 + type: string + publicationCredentialRef: + description: |- + PublicationCredentialRef identifies an operation-scoped Secret reference; + it never contains credential material. + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + sessionUid: + maxLength: 1024 + type: string + sourceBaselineSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + sourceRef: + description: SourceRef is the exact immutable source ref or revision + selector. + maxLength: 1024 + minLength: 1 + type: string + sourceRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + targetRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + targetRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + taskUid: + maxLength: 1024 + minLength: 1 + type: string + required: + - artifactDigest + - artifactId + - artifactMediaType + - artifactSizeBytes + - attempt + - baseline + - branchClaimGeneration + - branchClaimId + - commitIdentity + - commitMessage + - commitTimestamp + - generation + - id + - promptId + - publicationCredentialRef + - requestDigest + - sourceBaselineSha + - sourceRef + - sourceRepositoryId + - targetRef + - targetRepositoryId + - taskUid + type: object + x-kubernetes-validations: + - message: publication spec is immutable + rule: self == oldSelf + status: + description: PublicationStatus contains mutable state, exact receipts, + and epoch fencing. + properties: + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + prIntent: + description: |- + PublicationPullRequestIntent is the exact forge tuple persisted before the + first forge API call. + properties: + baseRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + baseRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + expectedHeadSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + headRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + headRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + publicationGeneration: + format: int64 + minimum: 1 + type: integer + required: + - baseRef + - baseRepositoryId + - expectedHeadSha + - headRef + - headRepositoryId + - publicationGeneration + type: object + preparedReceipt: + description: PreparedPublicationControlReceipt records deterministic + commit preparation. + properties: + bundleArtifactId: + maxLength: 1024 + minLength: 1 + type: string + bundleDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + bundleMediaType: + maxLength: 255 + minLength: 1 + type: string + bundleRef: + pattern: ^refs/orka/publications/[a-f0-9]{64}$ + type: string + bundleSizeBytes: + format: int64 + minimum: 1 + type: integer + commitSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + manifestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + preparedAt: + format: date-time + type: string + relativeRoot: + description: |- + RelativeRoot is the canonical repository-relative workspace root applied + to every path in the immutable delta artifact. + maxLength: 1024 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + treeSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - bundleArtifactId + - bundleDigest + - bundleMediaType + - bundleRef + - bundleSizeBytes + - commitSha + - manifestDigest + - operationId + - preparedAt + - requestDigest + - treeSha + type: object + publishReceipt: + description: PublishOperationControlReceipt records the exact server-enforced + ref CAS. + properties: + acknowledgementUnknown: + type: boolean + expectedCommitSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + publishedAt: + format: date-time + type: string + remoteBefore: + description: |- + ControlRemoteRefState is an exact remote-ref observation. Absent and SHA are + mutually exclusive. The all-zero value is reserved for an explicitly unknown + observation in PublicationOutcomeUnknown receipts. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + targetRef: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + targetRepositoryId: + maxLength: 1024 + minLength: 1 + type: string + required: + - acknowledgementUnknown + - expectedCommitSha + - operationId + - publishedAt + - remoteBefore + - requestDigest + - targetRef + - targetRepositoryId + type: object + pullRequestReceipt: + description: PullRequestOperationControlReceipt snapshots exact forge + reconciliation. + properties: + forgeId: + maxLength: 1024 + minLength: 1 + type: string + headSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + intentKey: + maxLength: 1024 + minLength: 1 + type: string + operationId: + maxLength: 1024 + minLength: 1 + type: string + reconciledAt: + format: date-time + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + state: + maxLength: 128 + minLength: 1 + type: string + url: + maxLength: 2048 + minLength: 1 + type: string + required: + - forgeId + - headSha + - intentKey + - operationId + - reconciledAt + - requestDigest + - state + - url + type: object + state: + description: PublicationControlState is the clean-room publication + state machine. + enum: + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - CancelledBeforePublish + - DeliveryConflict + - CredentialBlocked + - PreparationFailed + - PublicationOutcomeUnknown + type: string + terminalReason: + maxLength: 16384 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + verificationReceipt: + description: PublicationVerificationControlReceipt is an independent + remote observation. + properties: + descendantProofDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + expectedCommitSha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + observedRemote: + description: |- + ControlRemoteRefState is an exact remote-ref observation. Absent and SHA are + mutually exclusive. The all-zero value is reserved for an explicitly unknown + observation in PublicationOutcomeUnknown receipts. + properties: + absent: + type: boolean + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - absent + type: object + x-kubernetes-validations: + - message: absent and sha are mutually exclusive + rule: '!(self.absent && has(self.sha) && size(self.sha) > 0)' + operationId: + maxLength: 1024 + minLength: 1 + type: string + outcome: + description: PublicationControlState is the clean-room publication + state machine. + enum: + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - CancelledBeforePublish + - DeliveryConflict + - CredentialBlocked + - PreparationFailed + - PublicationOutcomeUnknown + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + verifiedAt: + format: date-time + type: string + required: + - expectedCommitSha + - observedRemote + - operationId + - outcome + - requestDigest + - verifiedAt + type: object + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: failure and unknown publication states require a terminal reason + rule: '!has(self.state) || !(self.state in [''DeliveryConflict'', ''CredentialBlocked'', + ''PreparationFailed'', ''PublicationOutcomeUnknown'']) || (has(self.terminalReason) + && size(self.terminalReason) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.orka.ai_runtimepools.yaml b/config/crd/bases/core.orka.ai_runtimepools.yaml new file mode 100644 index 000000000..3100c3d7d --- /dev/null +++ b/config/crd/bases/core.orka.ai_runtimepools.yaml @@ -0,0 +1,638 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: runtimepools.core.orka.ai +spec: + group: core.orka.ai + names: + kind: RuntimePool + listKind: RuntimePoolList + plural: runtimepools + shortNames: + - rtpool + singular: runtimepool + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.lifecycle + name: Lifecycle + type: string + - jsonPath: .status.admissionState + name: Admission + type: string + - jsonPath: .status.desiredReplicas + name: Desired + type: integer + - jsonPath: .status.currentReplicas + name: Current + type: integer + - jsonPath: .status.capacity.residentSessions + name: Sessions + type: integer + - jsonPath: .status.capacity.runningPrompts + name: Prompts + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: RuntimePool is the Schema for controller-owned ACP runtime pools. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + RuntimePoolSpec defines the desired state of a controller-owned logical pool. + Trust-domain placement and the runtime image/profile are immutable; rollout + uses drain-and-replace rather than changing an in-memory instance in place. + properties: + capacity: + default: + maxResidentSessions: 10 + maxRunningPrompts: 4 + description: Capacity sets resident-session and running-prompt limits. + properties: + maxResidentSessions: + default: 10 + description: MaxResidentSessions is the maximum number of resident + RuntimeSessions. + format: int32 + maximum: 1000 + minimum: 1 + type: integer + maxRunningPrompts: + default: 4 + description: MaxRunningPrompts is the maximum number of concurrently + running prompts. + format: int32 + maximum: 1000 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: maxRunningPrompts cannot exceed maxResidentSessions + rule: self.maxRunningPrompts <= self.maxResidentSessions + coldStartTimeoutSeconds: + default: 120 + description: ColdStartTimeoutSeconds bounds a 0 -> 1 startup before + the pool is marked degraded. + format: int32 + maximum: 3600 + minimum: 1 + type: integer + desiredReplicas: + default: 0 + description: |- + DesiredReplicas is zero or one. More than one runtime Pod would make + stateful exact-instance routing ambiguous. + format: int32 + maximum: 1 + minimum: 0 + type: integer + runtime: + description: Runtime pins the immutable supervisor image and behavior + profile. + properties: + image: + description: Image is a digest-pinned OCI image. Mutable tags + are intentionally rejected. + maxLength: 2048 + pattern: ^[^\s@]+@sha256:[a-f0-9]{64}$ + type: string + profile: + description: Profile is the immutable runtime profile enforced + for every active instance. + properties: + acpProfile: + description: ACPProfile is the reviewed ACP wire/profile identifier. + enum: + - acp.v1 + type: string + adapterDigests: + additionalProperties: + type: string + description: AdapterDigests pins every adapter and provider + CLI artifact used by the pool. + maxProperties: 32 + minProperties: 1 + type: object + agentConfigurationDigest: + description: AgentConfigurationDigest freezes non-secret Agent/runtime + configuration. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + approvalPolicyDigest: + description: ApprovalPolicyDigest freezes the effective approval + policy. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digest: + description: Digest is the canonical immutable runtime-profile + digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + digestSchemaVersion: + description: DigestSchemaVersion identifies the canonicalization + schema used to compute Digest. + maxLength: 64 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ + type: string + mcpConfigurationDigest: + description: MCPConfigurationDigest freezes prompt-scoped + broker/MCP configuration. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + model: + description: Model is the exact reviewed model identifier. + maxLength: 256 + minLength: 1 + type: string + protocolVersion: + default: orka.harness.v2 + description: ProtocolVersion is the controller-to-supervisor + protocol profile. + enum: + - orka.harness.v2 + type: string + providerKind: + description: ProviderKind selects the one provider adapter + present in the immutable image. + enum: + - codex + - claude + - copilot + type: string + proxyCredentialRole: + description: ProxyCredentialRole identifies the provider-proxy + client role, never a secret value. + maxLength: 256 + minLength: 1 + type: string + proxyCredentialScope: + description: ProxyCredentialScope is the bounded model/session + capability scope. + maxLength: 1024 + minLength: 1 + type: string + resourceClass: + description: ResourceClass is the controller-supported pool + resource class included in Digest. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$ + type: string + toolPolicyDigest: + description: ToolPolicyDigest freezes the effective tool allow/deny + policy. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + workspaceIntent: + description: WorkspaceIntent is part of the immutable runtime + profile. + enum: + - read + - write + type: string + required: + - acpProfile + - adapterDigests + - agentConfigurationDigest + - approvalPolicyDigest + - digest + - digestSchemaVersion + - mcpConfigurationDigest + - model + - providerKind + - proxyCredentialRole + - proxyCredentialScope + - resourceClass + - toolPolicyDigest + - workspaceIntent + type: object + required: + - image + - profile + type: object + runtimeNamespace: + description: |- + RuntimeNamespace is the physical namespace for controller-owned runtime + resources. When omitted, the controller selects its configured runtime namespace. + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + trustDomain: + description: TrustDomain is the logical namespace/identity boundary + served by this pool. + properties: + identity: + description: |- + Identity is the controller-defined, canonical trust-domain identity. It + must remain stable across physical runtime namespace or Pod replacement. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: Namespace is the Task namespace represented by this + trust domain. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - identity + - namespace + type: object + required: + - runtime + - trustDomain + type: object + x-kubernetes-validations: + - message: trustDomain is immutable + rule: self.trustDomain == oldSelf.trustDomain + - message: runtimeNamespace is immutable + rule: has(self.runtimeNamespace) == has(oldSelf.runtimeNamespace) && + (!has(self.runtimeNamespace) || self.runtimeNamespace == oldSelf.runtimeNamespace) + - message: runtime image and profile are immutable + rule: self.runtime == oldSelf.runtime + status: + description: RuntimePoolStatus defines the observed state of a controller-owned + pool. + properties: + activeInstance: + description: |- + ActiveInstance is the exact selected Pod and supervisor boot. It is empty + unless one instance has been authoritatively selected. + properties: + bootID: + description: BootID is the immutable supervisor boot identifier + inside the selected Pod. + maxLength: 128 + minLength: 1 + type: string + controllerEpoch: + description: ControllerEpoch is the durable controller epoch to + which this instance is bound. + format: int64 + minimum: 1 + type: integer + lastObservedTime: + description: LastObservedTime is the last authenticated status + observation for this instance. + format: date-time + type: string + podAddress: + description: |- + PodAddress is the exact Pod address used for stateful routing, not a + load-balanced Service endpoint. + maxLength: 253 + minLength: 1 + type: string + podName: + description: PodName is the exact selected runtime Pod name. + maxLength: 253 + minLength: 1 + type: string + podNamespace: + description: PodNamespace is the namespace containing the selected + runtime Pod. + maxLength: 63 + minLength: 1 + type: string + podUID: + description: PodUID is the Kubernetes UID of the selected Pod. + maxLength: 128 + minLength: 1 + type: string + profileDigest: + description: ProfileDigest is the immutable runtime-profile digest + advertised by this instance. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + profileDigestSchemaVersion: + description: ProfileDigestSchemaVersion is the digest schema advertised + by this instance. + maxLength: 64 + minLength: 1 + type: string + protocolVersion: + description: ProtocolVersion is the supervisor protocol actually + advertised by this instance. + enum: + - orka.harness.v2 + type: string + providerTokenGeneration: + description: |- + ProviderTokenGeneration is a non-secret digest generation for the exact + provider capability mounted into this runtime Pod. It lets the controller + prove that a selected instance converged on the intended proxy credential + without exposing the bearer token. + pattern: ^[a-f0-9]{16}$ + type: string + runtimeInstanceID: + description: RuntimeInstanceID is the portable v2 instance fence + derived from PodUID and BootID. + maxLength: 253 + minLength: 1 + type: string + required: + - bootID + - controllerEpoch + - podAddress + - podName + - podNamespace + - podUID + - profileDigest + - profileDigestSchemaVersion + - protocolVersion + - providerTokenGeneration + - runtimeInstanceID + type: object + admissionState: + description: AdmissionState is the authoritative admission gate for + new RuntimeSessions. + enum: + - Closed + - Accepting + - Draining + - Ambiguous + type: string + capacity: + description: Capacity reports effective limits, use, and queued demand. + properties: + finalizingSessions: + description: FinalizingSessions is the count reserved for validation, + publication, or finalization. + format: int32 + minimum: 0 + type: integer + liveDescendants: + description: LiveDescendants is the authenticated count of tracked + runtime descendants. + format: int32 + minimum: 0 + type: integer + maxResidentSessions: + description: MaxResidentSessions is the effective configured resident-session + limit. + format: int32 + minimum: 0 + type: integer + maxRunningPrompts: + description: MaxRunningPrompts is the effective configured running-prompt + limit. + format: int32 + minimum: 0 + type: integer + pendingPermissions: + description: PendingPermissions is the authenticated count of + unresolved prompt permissions. + format: int32 + minimum: 0 + type: integer + queuedTasks: + description: QueuedTasks is durable unsatisfied demand assigned + to this pool. + format: int32 + minimum: 0 + type: integer + reservations: + description: |- + Reservations is the bounded authoritative set of coordinator-owned + pre-admission capacity claims. + items: + description: |- + RuntimePoolCapacityReservationStatus is one durable, exact-instance capacity + claim. The composite key is the pool UID, Task UID, attempt, and controller + epoch. A reservation claims resident-session and prompt admission slots until + the supervisor accepts the corresponding work or the reservation expires. + properties: + attempt: + description: Attempt is the Task attempt that owns the claim. + format: int32 + minimum: 1 + type: integer + controllerEpoch: + description: ControllerEpoch fences the claim to one controller + leadership epoch. + format: int64 + minimum: 1 + type: integer + expiresAt: + description: |- + ExpiresAt is renewed while pre-admission work is active. A later + dispatcher may reclaim the claim after this time. + format: date-time + type: string + poolUID: + description: PoolUID fences the claim to the exact RuntimePool + object. + maxLength: 128 + minLength: 1 + type: string + promptSlots: + description: PromptSlots is one until the prompt is accepted + by the supervisor. + format: int32 + maximum: 1 + minimum: 0 + type: integer + reservedAt: + description: ReservedAt is the first successful resource-version + CAS for this claim. + format: date-time + type: string + residentSlots: + description: |- + ResidentSlots is zero after a RuntimeSession is admitted and one while a + new resident-session slot is still reserved. + format: int32 + maximum: 1 + minimum: 0 + type: integer + runtimeInstanceID: + description: RuntimeInstanceID binds admission to the exact + selected Pod/boot pair. + maxLength: 253 + minLength: 1 + type: string + taskUID: + description: TaskUID is the immutable Task identity that + owns the claim. + maxLength: 128 + minLength: 1 + type: string + required: + - attempt + - controllerEpoch + - expiresAt + - poolUID + - promptSlots + - reservedAt + - residentSlots + - runtimeInstanceID + - taskUID + type: object + x-kubernetes-validations: + - message: a capacity reservation must claim at least one slot + rule: self.residentSlots + self.promptSlots > 0 + maxItems: 1000 + type: array + x-kubernetes-list-map-keys: + - poolUID + - taskUID + - attempt + - controllerEpoch + x-kubernetes-list-type: map + reservedPrompts: + description: ReservedPrompts is the sum of prompt slots in Reservations. + format: int32 + minimum: 0 + type: integer + reservedSessions: + description: ReservedSessions is the sum of resident slots in + Reservations. + format: int32 + minimum: 0 + type: integer + residentSessions: + description: ResidentSessions is the authenticated supervisor + count of resident sessions. + format: int32 + minimum: 0 + type: integer + runningPrompts: + description: RunningPrompts is the authenticated supervisor count + of active prompts. + format: int32 + minimum: 0 + type: integer + type: object + conditions: + description: |- + Conditions report admission, Pod Security, quota, scheduling, rollout, and + other controller-observed failures. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerEpoch: + description: ControllerEpoch is the durable epoch required for authoritative + pool writes. + format: int64 + minimum: 0 + type: integer + currentReplicas: + description: CurrentReplicas is the number of non-terminated runtime + Pods owned by the pool. + format: int32 + minimum: 0 + type: integer + desiredReplicas: + description: DesiredReplicas is the desired replica count observed + by the controller. + format: int32 + maximum: 1 + minimum: 0 + type: integer + lifecycle: + description: Lifecycle is the explicit pool lifecycle. + enum: + - Stopped + - Starting + - Serving + - Draining + - Quiescent + - Stopping + - Degraded + - Ambiguous + type: string + message: + description: Message contains bounded, sanitized reconciliation context. + maxLength: 1024 + type: string + observedGeneration: + description: ObservedGeneration is the latest RuntimePool generation + reconciled by the controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.orka.ai_runtimesessioncontrols.yaml b/config/crd/bases/core.orka.ai_runtimesessioncontrols.yaml new file mode 100644 index 000000000..010bfd9d7 --- /dev/null +++ b/config/crd/bases/core.orka.ai_runtimesessioncontrols.yaml @@ -0,0 +1,309 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: runtimesessioncontrols.core.orka.ai +spec: + group: core.orka.ai + names: + kind: RuntimeSessionControl + listKind: RuntimeSessionControlList + plural: runtimesessioncontrols + shortNames: + - rsctrl + singular: runtimesessioncontrol + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.lifecycle + name: Lifecycle + type: string + - jsonPath: .status.availability + name: Availability + type: string + - jsonPath: .status.generation + name: Generation + type: integer + - jsonPath: .status.mutationLeaseGeneration + name: Lease + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + RuntimeSessionControl is the Kubernetes-authoritative RuntimeSession control + record. SessionTurn/transcript/deferred-outbox data remains in one durable + SQLite transaction; the Kubernetes store completes the authoritative + SessionControl/BranchClaim CAS before activating the terminal projection. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + RuntimeSessionControlSpec contains immutable session identity, ownership, and + profile bindings. Profile changes create a new session generation in status; + they do not mutate this immutable record identity. + properties: + owner: + description: Owner identifies the immutable Task or durable Session + owner. + properties: + kind: + enum: + - Task + - Session + - RuntimePool + - PromptAttempt + type: string + uid: + maxLength: 1024 + minLength: 1 + type: string + required: + - kind + - uid + type: object + profileDigestSchemaVersion: + description: ProfileDigestSchemaVersion identifies how RuntimeProfileDigest + was built. + maxLength: 64 + type: string + requestDigest: + description: RequestDigest binds creation to exact canonical input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimePoolRef: + description: RuntimePoolRef is the controller-owned logical pool name + when known. + maxLength: 253 + type: string + runtimePoolUid: + description: RuntimePoolUID fences the pool object across delete/recreate. + maxLength: 1024 + type: string + runtimeProfileDigest: + description: RuntimeProfileDigest binds the session to immutable runtime + behavior. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + sessionName: + description: |- + SessionName is the immutable user-visible Session key within the object + namespace. The Kubernetes object name is a digest-derived storage key and + must not be treated as the Session name. + maxLength: 1024 + minLength: 1 + type: string + sessionUid: + description: SessionUID is the immutable Orka Session identity. + maxLength: 1024 + minLength: 1 + type: string + required: + - owner + - requestDigest + - sessionName + - sessionUid + type: object + x-kubernetes-validations: + - message: runtime session control spec is immutable + rule: self == oldSelf + status: + description: |- + RuntimeSessionControlStatus contains the lifecycle, generation, mutation + Lease, and independently verified recovery baseline. + properties: + availability: + description: RuntimeSessionControlAvailability gates the Session mutation + lease. + enum: + - Available + - ReconciliationBlocked + type: string + blockedReason: + maxLength: 16384 + type: string + controllerEpoch: + description: ControllerEpoch is the exact epoch that performed the + last mutation. + format: int64 + minimum: 1 + type: integer + controllerEpochLeaseResourceVersion: + description: |- + ControllerEpochLeaseResourceVersion is the resourceVersion of the + authoritative controller-epoch Lease observed by the mutation. + maxLength: 64 + type: string + controllerEpochName: + description: |- + ControllerEpochName identifies the controller epoch domain checked before + the mutation. + maxLength: 253 + type: string + createdAt: + description: CreatedAt is the normalized logical creation time. + format: date-time + type: string + generation: + description: Generation is the monotonic ACP RuntimeSession generation. + format: int64 + minimum: 1 + type: integer + lastOperationDigest: + description: LastOperationDigest binds LastOperationID to exact canonical + input. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + lastOperationId: + description: LastOperationID is the last idempotent mutation identity + applied. + maxLength: 1024 + type: string + lifecycle: + description: RuntimeSessionControlLifecycle is the durable RuntimeSession + lifecycle. + enum: + - Creating + - Idle + - PromptRunning + - Validating + - PreparingPublication + - PublicationPrepared + - Publishing + - Verifying + - Finalizing + - Cancelling + - Poisoned + - Deleting + - Deleted + type: string + mutationLease: + description: |- + RuntimeSessionMutationLeaseStatus mirrors the namespaced Kubernetes Lease + that serializes mutation for one immutable SessionUID. + properties: + acquiredAt: + format: date-time + type: string + attempt: + format: int64 + minimum: 1 + type: integer + expiresAt: + format: date-time + type: string + generation: + format: int64 + minimum: 1 + type: integer + leaseName: + maxLength: 253 + minLength: 1 + type: string + leaseResourceVersion: + maxLength: 64 + minLength: 1 + type: string + promptId: + maxLength: 1024 + minLength: 1 + type: string + requestDigest: + pattern: ^sha256:[a-f0-9]{64}$ + type: string + taskUid: + maxLength: 1024 + minLength: 1 + type: string + required: + - acquiredAt + - attempt + - generation + - leaseName + - leaseResourceVersion + - promptId + - requestDigest + - taskUid + type: object + mutationLeaseGeneration: + description: MutationLeaseGeneration is monotonic and never reused + for SessionUID. + format: int64 + minimum: 0 + type: integer + relatedPromptAttemptId: + maxLength: 1024 + type: string + relatedPublicationId: + maxLength: 1024 + type: string + updatedAt: + description: UpdatedAt is the normalized logical mutation time. + format: date-time + type: string + verifiedBaseline: + description: ControlVerifiedBranchBaseline is an independently verified + branch baseline. + properties: + ref: + maxLength: 1024 + pattern: ^refs/heads/.+$ + type: string + repositoryId: + maxLength: 1024 + minLength: 1 + type: string + sha: + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + required: + - ref + - repositoryId + - sha + type: object + version: + description: |- + Version is the monotonic domain CAS version. It advances once for each + successfully persisted logical mutation. + format: int64 + minimum: 1 + type: integer + type: object + x-kubernetes-validations: + - message: available sessions must clear reconciliation block metadata + rule: '!has(self.availability) || self.availability != ''Available'' + || ((!has(self.blockedReason) || size(self.blockedReason) == 0) && + (!has(self.relatedPromptAttemptId) || size(self.relatedPromptAttemptId) + == 0) && (!has(self.relatedPublicationId) || size(self.relatedPublicationId) + == 0))' + - message: reconciliation-blocked sessions require a reason + rule: '!has(self.availability) || self.availability != ''ReconciliationBlocked'' + || (has(self.blockedReason) && size(self.blockedReason) > 0)' + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.orka.ai_tasks.yaml b/config/crd/bases/core.orka.ai_tasks.yaml index 704b92442..84b52b738 100644 --- a/config/crd/bases/core.orka.ai_tasks.yaml +++ b/config/crd/bases/core.orka.ai_tasks.yaml @@ -92,52 +92,6 @@ spec: maximum: 1000 minimum: 1 type: integer - workspace: - description: Workspace defines the working directory configuration - properties: - branch: - description: Branch is the git branch to checkout - type: string - forkRepo: - description: ForkRepo is the writable fork repository URL - for pushing changes - type: string - gitRepo: - description: GitRepo is the repository URL to clone - type: string - gitSecretRef: - description: GitSecretRef references a Secret containing git - credentials - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - prBaseBranch: - description: PRBaseBranch is the upstream branch to target - for pull requests - type: string - pushBranch: - description: |- - PushBranch is the remote branch name to push changes to after the agent completes. - When set, FinalizeResult will commit and push changes to this branch. - type: string - ref: - description: Ref is a specific git ref (commit SHA, tag) to - checkout - type: string - subPath: - description: SubPath is a subdirectory within the repo to - use as workspace root - type: string - type: object type: object ai: description: AI contains AI-specific configuration (when type is "ai") @@ -1367,10 +1321,10 @@ spec: type: array workspace: description: |- - Workspace requests an upstream agent-sandbox execution workspace for agent Tasks. - When enabled, the Task controller validates the request and propagates the - resolved sandbox settings to the agent worker Job. The worker wrapper then - claims the sandbox workspace and runs the configured agent runtime inside it. + Workspace requests an execution workspace for worker-backed Task types. + ACP core agent Tasks reject this field because their ephemeral workspace is + owned by RuntimeSession lifecycle and clean-room publication. Actor-backed + RuntimeSession support is a future integration behind the v2 lifecycle seam. properties: boot: description: |- @@ -1803,51 +1757,258 @@ spec: type: string workspace: description: |- - Workspace defines repository checkout and push settings for tasks that need - a git workspace. Agent tasks can continue to use agentRuntime.workspace for - compatibility; this top-level field is used by container tasks as well. + Workspace defines the canonical repository workspace, intent, credentials, + and publication request. Agent Tasks that omit intent are interpreted as + read by controller logic; an omitted intent preserves existing container behavior. properties: + allowedPaths: + description: |- + AllowedPaths restricts publishable workspace changes to these path globs or + directory prefixes ending in /**. Empty allows every otherwise-safe path. + It is not supported for container Tasks. + items: + type: string + maxItems: 256 + type: array branch: - description: Branch is the git branch to checkout + description: Branch is the source branch to check out. + maxLength: 255 type: string - forkRepo: - description: ForkRepo is the writable fork repository URL for - pushing changes + createPR: + default: false + description: |- + CreatePR explicitly requests pull request reconciliation after branch publication. + Branch push remains the minimum durable delivery when false. It is supported only + for agent Tasks using the trusted ACP publisher boundary. + type: boolean + denyRepositoryControlPaths: + description: |- + DenyRepositoryControlPaths rejects workflow, RBAC, and chart-secret paths + before publication even when AllowedPaths is empty or otherwise matches. + It is not supported for container Tasks. + type: boolean + expectedRemoteSHA: + description: |- + ExpectedRemoteSHA requires the publication branch to exist at this exact + commit before publication. Empty means the branch must be absent. It is + supported only for agent Tasks using the trusted ACP publisher boundary. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ type: string + forgeCredentialRef: + description: |- + ForgeCredentialRef references the forge API credential used only for pull + request reconciliation when createPR=true. + properties: + key: + default: token + description: |- + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object gitRepo: - description: GitRepo is the repository URL to clone + description: |- + GitRepo is the source repository URL cloned by the clean-room workspace boundary. + Credentials must not be embedded in the URL. + maxLength: 2048 + type: string + intent: + description: |- + Intent declares whether the verified workspace must remain unchanged or may + produce a publication artifact. It is immutable for the lifetime of the Task. + Agent Tasks that omit intent are interpreted as read by controller logic; + omitted intent preserves the existing behavior of container Tasks. + enum: + - read + - write type: string - gitSecretRef: - description: GitSecretRef references a Secret containing git credentials + maxChangedFiles: + description: |- + MaxChangedFiles bounds the total changed, deleted, and symlink paths accepted + from the trusted supervisor before publication. Zero uses the runtime limit. + It is not supported for container Tasks. + format: int32 + minimum: 1 + type: integer + prBaseBranch: + description: PRBaseBranch is the upstream branch targeted when + CreatePR is true. + maxLength: 255 + type: string + publicationCredentialRef: + description: |- + PublicationCredentialRef references the target-repository write credential + used only for the exact CAS push. It is never used to clone the source. properties: - name: - default: "" + key: + default: token description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + required: + - name type: object - x-kubernetes-map-type: atomic - prBaseBranch: - description: PRBaseBranch is the upstream branch to target for - pull requests + publicationGitRepo: + description: |- + PublicationGitRepo is the repository URL whose branch receives an exact CAS publication. + Credentials must not be embedded in the URL. + maxLength: 2048 type: string + publicationReadCredentialRef: + description: |- + PublicationReadCredentialRef references the target-repository read + credential used only for preflight and independent post-push verification. + properties: + key: + default: token + description: |- + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + publicationRepository: + description: |- + PublicationRepository is the optional URL-derived identity for + PublicationGitRepo. When set, it must match the normalized credential-free + URL; for GitHub, use provider "github" and ID "github.com/owner/repo". + properties: + id: + description: |- + ID is the canonical credential-free URL identity and must match the + corresponding repository URL after normalization. For GitHub, use + "github.com/owner/repo"; GitHub GraphQL node IDs are not accepted. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object pushBranch: description: |- - PushBranch is the remote branch name to push changes to after the agent completes. - When set, FinalizeResult will commit and push changes to this branch. + PushBranch is the publication branch. For write Tasks the controller derives + a full-entropy Task- or Session-owned branch when this is omitted. + maxLength: 255 type: string + readCredentialRef: + description: |- + ReadCredentialRef references the one-operation clone/read credential Secret. + The Secret is resolved only by the clean-room workspace boundary. + properties: + key: + default: token + description: |- + Key is the Secret data key containing one bearer token or one complete + Authorization header. It defaults to "token" when omitted. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9._-]+$ + type: string + name: + description: Name is the name of the Secret in the Task namespace. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object ref: - description: Ref is a specific git ref (commit SHA, tag) to checkout + description: Ref is a specific source git ref, commit SHA, or + tag to check out. + maxLength: 512 type: string + rejectBinaryFiles: + description: |- + RejectBinaryFiles rejects changed file content that is not valid text. It is + not supported for container Tasks. + type: boolean + rejectSecretLikeContent: + description: |- + RejectSecretLikeContent applies Orka's generic secret detector to changed + paths and file contents before publication. It is not supported for container Tasks. + type: boolean + sourceRepository: + description: |- + SourceRepository is the optional URL-derived identity for GitRepo. When set, + it must match the normalized credential-free URL; for GitHub, use provider + "github" and ID "github.com/owner/repo". + properties: + id: + description: |- + ID is the canonical credential-free URL identity and must match the + corresponding repository URL after normalization. For GitHub, use + "github.com/owner/repo"; GitHub GraphQL node IDs are not accepted. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object subPath: - description: SubPath is a subdirectory within the repo to use - as workspace root + description: SubPath is a subdirectory within the source repository + used as workspace root. + maxLength: 1024 type: string type: object + x-kubernetes-validations: + - message: createPR requires write workspace intent + rule: '!self.createPR || self.intent == ''write''' + - message: gitRepo must not contain embedded credentials, query parameters, + or fragments + rule: '!has(self.gitRepo) || (!self.gitRepo.matches(''(?i)^[A-Za-z][A-Za-z0-9+.-]*://[^/]*@'') + && !self.gitRepo.contains(''?'') && !self.gitRepo.contains(''#''))' + - message: publicationGitRepo must not contain embedded credentials, + query parameters, or fragments + rule: '!has(self.publicationGitRepo) || (!self.publicationGitRepo.matches(''(?i)^[A-Za-z][A-Za-z0-9+.-]*://[^/]*@'') + && !self.publicationGitRepo.contains(''?'') && !self.publicationGitRepo.contains(''#''))' required: - type type: object @@ -1858,9 +2019,51 @@ spec: - message: transaction is immutable rule: has(self.transaction) == has(oldSelf.transaction) && (!has(self.transaction) || self.transaction == oldSelf.transaction) + - message: type is immutable + rule: self.type == oldSelf.type + - message: effective workspace intent is immutable + rule: '(has(self.workspace) && has(self.workspace.intent) ? self.workspace.intent + : (self.type == ''agent'' ? ''read'' : self.type)) == (has(oldSelf.workspace) + && has(oldSelf.workspace.intent) ? oldSelf.workspace.intent : (oldSelf.type + == ''agent'' ? ''read'' : oldSelf.type))' + - message: agent prompt is immutable + rule: self.type != 'agent' || (has(self.prompt) == has(oldSelf.prompt) + && (!has(self.prompt) || self.prompt == oldSelf.prompt)) + - message: agentRef is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.agentRef) == has(oldSelf.agentRef) + && (!has(self.agentRef) || self.agentRef == oldSelf.agentRef)) + - message: agentRuntime is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.agentRuntime) == has(oldSelf.agentRuntime) + && (!has(self.agentRuntime) || self.agentRuntime == oldSelf.agentRuntime)) + - message: sessionRef is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.sessionRef) == has(oldSelf.sessionRef) + && (!has(self.sessionRef) || self.sessionRef == oldSelf.sessionRef)) + - message: workspace is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.workspace) == has(oldSelf.workspace) + && (!has(self.workspace) || self.workspace == oldSelf.workspace)) + - message: timeout is immutable for agent Tasks + rule: self.type != 'agent' || (has(self.timeout) == has(oldSelf.timeout) + && (!has(self.timeout) || self.timeout == oldSelf.timeout)) - message: session workspace reuse requires spec.sessionRef rule: '!has(self.execution) || !has(self.execution.workspace) || self.execution.workspace.reusePolicy != ''session'' || has(self.sessionRef)' + - message: container Tasks do not support workspace.expectedRemoteSHA + rule: self.type != 'container' || !has(self.workspace) || !has(self.workspace.expectedRemoteSHA) + - message: container Tasks do not support workspace.createPR + rule: self.type != 'container' || !has(self.workspace) || (!has(self.workspace.createPR) + || !self.workspace.createPR) + - message: container Tasks do not support clean-room workspace publication + policies + rule: self.type != 'container' || !has(self.workspace) || (!has(self.workspace.maxChangedFiles) + && (!has(self.workspace.allowedPaths) || self.workspace.allowedPaths.size() + == 0) && (!has(self.workspace.denyRepositoryControlPaths) || !self.workspace.denyRepositoryControlPaths) + && (!has(self.workspace.rejectBinaryFiles) || !self.workspace.rejectBinaryFiles) + && (!has(self.workspace.rejectSecretLikeContent) || !self.workspace.rejectSecretLikeContent)) + - message: custom-image container Tasks do not support workspace.pushBranch + publication + rule: self.type != 'container' || !has(self.workspace) || !has(self.workspace.pushBranch) + || self.workspace.pushBranch.size() == 0 || !has(self.image) || self.image.size() + == 0 status: description: TaskStatus defines the observed state of Task properties: @@ -1965,10 +2168,383 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + delivery: + description: Delivery reports trusted workspace validation and publication + reconciliation. + properties: + artifactDigest: + description: ArtifactDigest is the durable content-addressed workspace + delta digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + branch: + description: Branch is the publication branch without a refs/heads/ + prefix. + maxLength: 255 + type: string + expectedCommitSHA: + description: ExpectedCommitSHA is the exact Orka-owned commit + prepared for publication. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + lastTransitionTime: + description: LastTransitionTime is the last durable delivery-state + transition time. + format: date-time + type: string + message: + description: Message contains bounded, sanitized delivery context. + maxLength: 1024 + type: string + outcome: + description: Outcome is set only after delivery reaches a terminal + classification. + enum: + - NotRequested + - VerifiedExact + - DeliveredSuperseded + - ReadValidated + - NoChange + - CancelledBeforePublish + - ReadOnlyWorkspaceModified + - DeliveryConflict + - CredentialBlocked + - PublicationOutcomeUnknown + type: string + prReceipt: + description: PRReceipt is present only when createPR was explicitly + requested and reconciled. + properties: + baseBranch: + description: BaseBranch is the reconciled pull request base + branch. + maxLength: 255 + type: string + headBranch: + description: HeadBranch is the reconciled pull request head + branch. + maxLength: 255 + type: string + headSHA: + description: HeadSHA is the exact observed pull request head + commit. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + id: + description: ID is the provider's durable pull request identifier. + maxLength: 512 + minLength: 1 + type: string + number: + description: Number is the provider's numeric pull request + number when available. + format: int64 + minimum: 1 + type: integer + state: + description: State is the provider-observed pull request state. + maxLength: 64 + type: string + url: + description: URL is the canonical user-facing pull request + URL. It must not contain credentials. + maxLength: 2048 + type: string + required: + - id + type: object + publicationID: + description: PublicationID is the durable identity reused for + reconciliation of the same artifact. + maxLength: 253 + pattern: ^[A-Za-z0-9][A-Za-z0-9._:-]{0,252}$ + type: string + publicationRepository: + description: PublicationRepository is the canonical repository + whose branch was reconciled. + properties: + id: + description: |- + ID is the canonical credential-free URL identity and must match the + corresponding repository URL after normalization. For GitHub, use + "github.com/owner/repo"; GitHub GraphQL node IDs are not accepted. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object + reason: + description: Reason is a stable machine-readable explanation for + State or Outcome. + maxLength: 128 + pattern: ^[A-Za-z][A-Za-z0-9._-]{0,127}$ + type: string + remoteBeforeSHA: + description: |- + RemoteBeforeSHA is the exact publication ref observed before the CAS push. + Nil means not yet observed; a pointer to the empty string records explicit + absence; a non-empty value records the observed object ID. + pattern: ^(|[a-f0-9]{40}|[a-f0-9]{64})$ + type: string + sourceRepository: + description: SourceRepository is the canonical repository from + which the workspace baseline was created. + properties: + id: + description: |- + ID is the canonical credential-free URL identity and must match the + corresponding repository URL after normalization. For GitHub, use + "github.com/owner/repo"; GitHub GraphQL node IDs are not accepted. + maxLength: 512 + minLength: 1 + type: string + provider: + description: Provider identifies the source-control provider + or forge. + maxLength: 63 + minLength: 1 + pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ + type: string + required: + - id + - provider + type: object + startingSHA: + description: StartingSHA is the verified source baseline before + prompt execution. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + state: + description: State is the current durable delivery state. + enum: + - NotRequested + - Validating + - Preparing + - Prepared + - Publishing + - Verifying + - VerifiedExact + - DeliveredSuperseded + - ReadValidated + - NoChange + - CancelledBeforePublish + - ReadOnlyWorkspaceModified + - DeliveryConflict + - CredentialBlocked + - PublicationOutcomeUnknown + type: string + supersedingRemoteSHA: + description: SupersedingRemoteSHA is the verified descendant that + superseded ExpectedCommitSHA. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + treeSHA: + description: TreeSHA is the deterministic clean-room tree written + by the publisher. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + verifiedRemoteSHA: + description: VerifiedRemoteSHA is the independently observed remote + branch head. + pattern: ^([a-f0-9]{40}|[a-f0-9]{64})$ + type: string + type: object + x-kubernetes-validations: + - message: delivery outcome requires the matching terminal state + rule: '!has(self.outcome) || (has(self.state) && self.state == self.outcome)' + - message: terminal delivery state requires an outcome + rule: '!has(self.state) || !(self.state in [''NotRequested'', ''VerifiedExact'', + ''DeliveredSuperseded'', ''ReadValidated'', ''NoChange'', ''CancelledBeforePublish'', + ''ReadOnlyWorkspaceModified'', ''DeliveryConflict'', ''CredentialBlocked'', + ''PublicationOutcomeUnknown'']) || has(self.outcome)' + execution: + description: |- + Execution reports the durable execution state and terminal outcome for the + current attempt. Phase remains the compatibility projection. + properties: + agentRuntimeName: + description: |- + AgentRuntimeName is the namespaced external orka.harness.v2 registration + selected for this attempt. It is mutually exclusive with RuntimePoolName. + maxLength: 253 + type: string + agentRuntimeUID: + description: |- + AgentRuntimeUID is the immutable external AgentRuntime UID fenced into the + attempt selection. + maxLength: 253 + type: string + attempt: + description: Attempt is the one-based Task execution attempt represented + by this status. + format: int32 + minimum: 1 + type: integer + controllerEpoch: + description: ControllerEpoch is the durable controller epoch fencing + this attempt. + format: int64 + minimum: 0 + type: integer + forgeCredentialResourceVersion: + description: |- + ForgeCredentialResourceVersion freezes the forge-only Secret version used + for pull request reconciliation. + maxLength: 253 + type: string + lastTransitionTime: + description: LastTransitionTime is the last durable execution-state + transition time. + format: date-time + type: string + message: + description: Message contains bounded, sanitized execution context. + maxLength: 1024 + type: string + outcome: + description: Outcome is set only after execution reaches a terminal + classification. + enum: + - Succeeded + - Failed + - Cancelled + - OutcomeUnknown + type: string + promptID: + description: PromptID is the durable prompt identity used for + submission and settlement. + maxLength: 253 + type: string + publicationCredentialResourceVersion: + description: |- + PublicationCredentialResourceVersion freezes the target-write Secret + version selected at reservation without exposing credential material. + maxLength: 253 + type: string + publicationReadCredentialResourceVersion: + description: |- + PublicationReadCredentialResourceVersion freezes the target-read Secret + version used for preflight and independent verification. + maxLength: 253 + type: string + readCredentialResourceVersion: + description: |- + ReadCredentialResourceVersion freezes the read credential Secret version + selected at reservation without exposing credential material. + maxLength: 253 + type: string + reason: + description: Reason is a stable machine-readable explanation for + State or Outcome. + maxLength: 128 + pattern: ^[A-Za-z][A-Za-z0-9._-]{0,127}$ + type: string + requestDigest: + description: RequestDigest is the canonical immutable prompt request + digest. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimeInstanceID: + description: RuntimeInstanceID is the exact selected supervisor + Pod UID plus boot identity. + maxLength: 512 + type: string + runtimePoolName: + description: RuntimePoolName is the namespaced logical pool selected + for this attempt. + maxLength: 253 + type: string + runtimePoolUID: + description: RuntimePoolUID is the immutable pool UID fenced into + runtime requests. + maxLength: 253 + type: string + runtimeSessionCleanupDigest: + description: |- + RuntimeSessionCleanupDigest is the controller-owned proof that the exact + RuntimeSession requiring retirement was deleted or its immutable runtime + instance was replaced. Users may read but cannot mutate the Task status subresource. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimeSessionGeneration: + description: RuntimeSessionGeneration is the monotonic profile/session + generation. + format: int64 + minimum: 0 + type: integer + runtimeSessionMCPDigest: + description: |- + RuntimeSessionMCPDigest binds the complete non-secret effective MCP policy + and descriptor configuration to a pending or reusable Session generation. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimeSessionProfileDigest: + description: |- + RuntimeSessionProfileDigest freezes the immutable runtime behavior bound to + a pending or reusable Session generation. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + runtimeSessionRecreationPending: + description: |- + RuntimeSessionRecreationPending records that the exact generation is being + created or replaced and must be reconciled before a different request may + reuse that identity. + type: boolean + runtimeSessionSupervisorBootID: + description: |- + RuntimeSessionSupervisorBootID freezes the supervisor boot that owns a + pending or reusable Session generation. + maxLength: 512 + type: string + runtimeSessionUID: + description: RuntimeSessionUID is the stable controller-owned + Session execution identity. + maxLength: 253 + type: string + runtimeSessionWorkspaceDigest: + description: |- + RuntimeSessionWorkspaceDigest binds a reusable Session generation to the + exact repository, source ref, verified baseline, intent, and relative root. + It contains no credential material. + pattern: ^sha256:[a-f0-9]{64}$ + type: string + state: + description: State is the current durable execution state. + enum: + - Queued + - Reserved + - SessionStarting + - Planned + - Submitting + - SubmittedUnknown + - Accepted + - Running + - Settling + - Succeeded + - Failed + - Cancelled + - OutcomeUnknown + type: string + type: object + x-kubernetes-validations: + - message: execution outcome requires the matching terminal state + rule: '!has(self.outcome) || (has(self.state) && self.state == self.outcome)' + - message: terminal execution state requires an outcome + rule: '!has(self.state) || !(self.state in [''Succeeded'', ''Failed'', + ''Cancelled'', ''OutcomeUnknown'']) || has(self.outcome)' executionOutcome: description: |- - ExecutionOutcome is the immutable outcome recorded when workload execution ends. Workspace - attachment revocation and cleanup continue independently while the Task is Finalizing. + ExecutionOutcome records the immutable outcome of a non-ACP workload before + provider-neutral execution-workspace finalization completes. properties: attempt: description: Attempt is the Task attempt that produced this outcome. @@ -2238,46 +2814,6 @@ spec: - name type: object type: object - harnessRuntime: - description: |- - HarnessRuntime records the controller-resolved harness runtime target for an - in-flight agent turn. It intentionally stores only non-secret routing metadata - and Secret references, never bearer values. - properties: - authRefField: - description: AuthRefField is the Secret data field selected when - the turn started. - type: string - authRefName: - description: AuthRefName is the Secret name selected when the - turn started. - type: string - authRefResourceVersion: - description: AuthRefResourceVersion is the auth Secret resourceVersion - validated before starting the turn. - type: string - contractVersion: - description: ContractVersion is the Orka harness contract version - used for the turn. - type: string - endpoint: - description: Endpoint is the non-secret harness base URL selected - when the turn started. - type: string - runtimeGeneration: - description: RuntimeGeneration is the AgentRuntime generation - selected when the turn started. - format: int64 - type: integer - runtimeName: - description: RuntimeName is the runtime name advertised by the - harness capabilities and sent in turn metadata. - type: string - runtimeRefName: - description: RuntimeRefName is the AgentRuntime name for custom - runtimeRef turns. Empty means built-in CLI wrapper. - type: string - type: object iteration: description: |- Iteration is the current autonomous loop iteration (0-based). diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 51452729c..aba9d7ef4 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,13 @@ resources: - bases/core.orka.ai_tools.yaml - bases/core.orka.ai_agents.yaml - bases/core.orka.ai_agentruntimes.yaml +- bases/core.orka.ai_runtimepools.yaml +- bases/core.orka.ai_promptattempts.yaml +- bases/core.orka.ai_runtimesessioncontrols.yaml +- bases/core.orka.ai_branchclaims.yaml +- bases/core.orka.ai_publications.yaml +- bases/core.orka.ai_controllerepochs.yaml +- bases/core.orka.ai_externaleffects.yaml - bases/core.orka.ai_providers.yaml - bases/core.orka.ai_skills.yaml - bases/core.orka.ai_repositoryscans.yaml diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index a989c4553..2cd921465 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -21,9 +21,12 @@ resources: - ../crd - ../rbac - ../manager -- ../harness-wrapper +- ../publisher +- ../provider-proxy +- ../scm-egress-proxy - ../admission - ../policy + # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook diff --git a/config/harness-wrapper/README.md b/config/harness-wrapper/README.md deleted file mode 100644 index 1bc05c13f..000000000 --- a/config/harness-wrapper/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Harness wrapper authentication Secret - -The canonical Kustomize installer intentionally does **not** commit or generate -the shared bearer token. Before applying `deploy/orka.yaml` directly, create the -required Secret in `orka-system` without printing the token: - -```bash -set -euo pipefail - -kubectl create namespace orka-system --dry-run=client -o yaml | kubectl apply -f - -if ! kubectl -n orka-system get secret harness-wrapper-auth >/dev/null 2>&1; then - openssl rand -hex 32 | \ - kubectl -n orka-system create secret generic harness-wrapper-auth \ - --from-file=token=/dev/stdin -fi - -kubectl apply -f deploy/orka.yaml -``` - -`make deploy` performs the same preflight and creates the Secret only when it is -absent. Helm installs use the chart-managed Secret or -`workers.harnessWrapper.auth.existingSecret` instead. diff --git a/config/harness-wrapper/deployment.yaml b/config/harness-wrapper/deployment.yaml deleted file mode 100644 index 0e29a2250..000000000 --- a/config/harness-wrapper/deployment.yaml +++ /dev/null @@ -1,110 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-harness-wrapper - labels: - app.kubernetes.io/name: orka - app.kubernetes.io/component: agent-harness-wrapper - app.kubernetes.io/managed-by: kustomize -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app.kubernetes.io/name: orka - app.kubernetes.io/component: agent-harness-wrapper - template: - metadata: - labels: - app.kubernetes.io/name: orka - app.kubernetes.io/component: agent-harness-wrapper - spec: - serviceAccountName: agent-harness-wrapper - automountServiceAccountToken: false - securityContext: - # The wrapper process runs as root only so it can read 0400 controller - # token volumes and set child credentials. Runtime commands are launched - # through commandSysProcAttr with ORKA_HARNESS_WRAPPER_CHILD_UID/GID. - runAsUser: 0 - runAsGroup: 0 - seccompProfile: - type: RuntimeDefault - containers: - - name: wrapper - image: ghcr.io/orka-agents/orka/agent-harness-wrapper:0.1.1 - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 8080 - env: - - name: ORKA_HARNESS_WRAPPER_RUNTIME - value: multi - - name: ORKA_HARNESS_WRAPPER_LISTEN_ADDR - value: :8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_ALLOW_BASH - value: "true" - # All turn subprocesses run under this unprivileged identity. - - name: ORKA_HARNESS_WRAPPER_CHILD_UID - value: "1000" - - name: ORKA_HARNESS_WRAPPER_CHILD_GID - value: "1000" - - name: ORKA_CODEX_SANDBOX_MODE - value: danger-full-access - - name: ORKA_SA_TOKEN_PATH - value: /var/run/orka/upload-token/token - volumeMounts: - - name: auth - mountPath: /var/run/orka/harness-wrapper - readOnly: true - - name: upload-token - mountPath: /var/run/orka/upload-token - readOnly: true - - name: tmp - mountPath: /tmp - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - add: - - SETUID - - SETGID - - CHOWN - - KILL - - FOWNER - livenessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 10 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: "2" - memory: 2Gi - volumes: - - name: upload-token - projected: - defaultMode: 0400 - sources: - - serviceAccountToken: - path: token - - name: auth - secret: - secretName: harness-wrapper-auth - defaultMode: 0400 - - name: tmp - emptyDir: {} diff --git a/config/harness-wrapper/kustomization.yaml b/config/harness-wrapper/kustomization.yaml deleted file mode 100644 index 23ca489e6..000000000 --- a/config/harness-wrapper/kustomization.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -# `harness-wrapper-auth` must be pre-created; see README.md. -resources: -- serviceaccount.yaml -- deployment.yaml -- service.yaml - -patches: -- path: volume-mode-patch.yaml diff --git a/config/harness-wrapper/serviceaccount.yaml b/config/harness-wrapper/serviceaccount.yaml deleted file mode 100644 index 3f00a64b8..000000000 --- a/config/harness-wrapper/serviceaccount.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: agent-harness-wrapper - labels: - app.kubernetes.io/name: orka - app.kubernetes.io/component: agent-harness-wrapper - app.kubernetes.io/managed-by: kustomize diff --git a/config/harness-wrapper/volume-mode-patch.yaml b/config/harness-wrapper/volume-mode-patch.yaml deleted file mode 100644 index bcc862efa..000000000 --- a/config/harness-wrapper/volume-mode-patch.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: agent-harness-wrapper -spec: - template: - spec: - volumes: - - name: upload-token - projected: - defaultMode: 0400 - - name: auth - secret: - defaultMode: 0400 diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 1b48346e4..f870ecaa2 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,6 +1,7 @@ resources: - manager.yaml - store-pvc.yaml + apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c429eec25..14a52bfc7 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -34,6 +34,7 @@ spec: labels: control-plane: controller-manager app.kubernetes.io/name: orka + orka.ai/network-role: controller spec: # TODO(user): Uncomment the following code to configure the nodeAffinity expression # according to the platforms which are supported by your solution. @@ -72,6 +73,16 @@ spec: - --store-backend=sqlite - --store-path=/data/orka.db - --controller-url=http://orka-api.orka-system.svc:8080 + - --acp-runtime-enabled=true + - --acp-upgrade-drain-bind-address=127.0.0.1:8083 + - --acp-upgrade-drain-timeout=5m + - --acp-upgrade-drain-poll-interval=1s + - --acp-upgrade-drain-trigger-timeout=5m15s + - --acp-upgrade-drain-marker-namespace=orka-system + - --acp-provider-proxy-base-url=http://orka-provider-auth-proxy.orka-system.svc:8080 + - --acp-provider-proxy-namespace=orka-system + - --acp-provider-proxy-pod-labels=orka.ai/network-role=provider-auth-proxy + - --acp-provider-proxy-token-file=/var/run/orka/provider-auth/token - --ai-worker-image=ghcr.io/orka-agents/orka/ai-worker:0.1.1 - --general-worker-image=ghcr.io/orka-agents/orka/general-worker:0.1.1 - --gateway-enabled=true @@ -82,6 +93,7 @@ spec: - --gateway-terminal-retention=720h - --gateway-delivery-timeout=15s - --gateway-delivery-max-attempts=10 + - --execution-workspace-default-provider=agent-sandbox - --agent-sandbox-enabled=false - --agent-sandbox-warm-pool-policy=disabled @@ -100,12 +112,31 @@ spec: imagePullPolicy: IfNotPresent name: manager env: - - name: ORKA_HARNESS_WRAPPER_ENDPOINT - value: http://orka-agent-harness-wrapper:8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_HARNESS_WRAPPER_SERVICE_ACCOUNT_NAME - value: orka-agent-harness-wrapper + - name: ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE + value: /var/run/orka/acp-artifacts/capability-secret + - name: ORKA_ACP_ARTIFACT_ROOT + value: /data/acp-artifacts + - name: ORKA_ACP_CODEX_RUNTIME_IMAGE + valueFrom: + configMapKeyRef: + name: acp-runtime-images + key: ORKA_ACP_CODEX_RUNTIME_IMAGE + - name: ORKA_ACP_CLAUDE_RUNTIME_IMAGE + valueFrom: + configMapKeyRef: + name: acp-runtime-images + key: ORKA_ACP_CLAUDE_RUNTIME_IMAGE + - name: ORKA_ACP_COPILOT_RUNTIME_IMAGE + valueFrom: + configMapKeyRef: + name: acp-runtime-images + key: ORKA_ACP_COPILOT_RUNTIME_IMAGE + - name: ORKA_WORKSPACE_PUBLISHER_URL + value: http://orka-workspace-publisher.orka-system.svc:8080 + - name: ORKA_WORKSPACE_PUBLISHER_CONTROLLER_TOKEN_FILE + value: /var/run/orka/publisher-auth/controller-token + - name: ORKA_WORKSPACE_PUBLISHER_CAPABILITY_SECRET_FILE + value: /var/run/orka/publisher-auth/operation-capability-secret ports: [] securityContext: readOnlyRootFilesystem: true @@ -113,6 +144,13 @@ spec: capabilities: drop: - "ALL" + lifecycle: + preStop: + exec: + command: + - /manager + - --acp-upgrade-drain-trigger-url=http://127.0.0.1:8083/acp/upgrade-drain + - --acp-upgrade-drain-trigger-timeout=5m15s livenessProbe: httpGet: path: /healthz @@ -139,8 +177,14 @@ spec: mountPath: /data - name: tmp mountPath: /tmp - - name: harness-wrapper-auth - mountPath: /var/run/orka/harness-wrapper + - name: acp-artifact-capability + mountPath: /var/run/orka/acp-artifacts + readOnly: true + - name: workspace-publisher-auth + mountPath: /var/run/orka/publisher-auth + readOnly: true + - name: provider-auth-proxy + mountPath: /var/run/orka/provider-auth readOnly: true volumes: - name: store @@ -148,8 +192,23 @@ spec: claimName: controller-manager-store - name: tmp emptyDir: {} - - name: harness-wrapper-auth + - name: acp-artifact-capability + secret: + secretName: acp-artifact-capability + defaultMode: 0400 + items: + - key: capability-secret + path: capability-secret + - name: workspace-publisher-auth + secret: + secretName: workspace-publisher-auth + defaultMode: 0400 + - name: provider-auth-proxy secret: - secretName: harness-wrapper-auth + secretName: provider-auth-proxy + defaultMode: 0400 + items: + - key: token + path: token serviceAccountName: controller-manager - terminationGracePeriodSeconds: 10 + terminationGracePeriodSeconds: 360 diff --git a/config/provider-proxy/deployment.yaml b/config/provider-proxy/deployment.yaml new file mode 100644 index 000000000..ab14a6616 --- /dev/null +++ b/config/provider-proxy/deployment.yaml @@ -0,0 +1,99 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: provider-auth-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy + template: + metadata: + # Token Secret updates are reloaded in-process. Change this nonce only to + # force a Pod rollout for binary/flag changes or operator recovery. + annotations: + orka.ai/provider-auth-rollout-nonce: "0" + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy + orka.ai/network-role: provider-auth-proxy + spec: + serviceAccountName: provider-auth-proxy + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: proxy + image: controller:latest + command: [/provider-auth-proxy] + args: + - --listen-address=:8080 + - --upstream-base-url=http://vekil.vekil-system.svc:1337 + - --token-file=/var/run/secrets/orka/provider-auth/token + - --previous-token-file=/var/run/secrets/orka/provider-auth/previous-token + - --previous-token-valid-until-file=/var/run/secrets/orka/provider-auth/previous-token-valid-until + - --token-reload-interval=5s + - --previous-token-overlap=10m + ports: + - name: http + containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] + resources: + requests: + cpu: 25m + memory: 32Mi + ephemeral-storage: 32Mi + limits: + cpu: 250m + memory: 128Mi + ephemeral-storage: 128Mi + readinessProbe: + httpGet: + path: /readyz + port: http + livenessProbe: + httpGet: + path: /healthz + port: http + volumeMounts: + - name: provider-auth + mountPath: /var/run/secrets/orka/provider-auth + readOnly: true + volumes: + - name: provider-auth + projected: + # The source is optional so key/Secret removal becomes an empty + # projection that the process detects and rejects, rather than + # leaving a stale last-known token mounted. fsGroup + 0440 grants + # only this non-root Pod group read access. + defaultMode: 0440 + sources: + - secret: + name: provider-auth-proxy + optional: true + items: + - key: token + path: token + # Optional for backward compatibility with existing + # single-token Secrets. During rotation, add both keys. The + # absolute RFC3339 deadline prevents Pod restarts from + # extending the configured overlap window. + - key: previous-token + path: previous-token + - key: previous-token-valid-until + path: previous-token-valid-until diff --git a/config/provider-proxy/kustomization.yaml b/config/provider-proxy/kustomization.yaml new file mode 100644 index 000000000..47d648cb4 --- /dev/null +++ b/config/provider-proxy/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - serviceaccount.yaml + - service.yaml + - deployment.yaml + - networkpolicy.yaml +images: + - name: controller + newName: ghcr.io/orka-agents/orka + newTag: latest diff --git a/config/provider-proxy/networkpolicy.yaml b/config/provider-proxy/networkpolicy.yaml new file mode 100644 index 000000000..6b7f5f9d1 --- /dev/null +++ b/config/provider-proxy/networkpolicy.yaml @@ -0,0 +1,44 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: provider-auth-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: orka-runtimes + podSelector: + matchLabels: + orka.ai/network-role: provider-client + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - {protocol: UDP, port: 53} + - {protocol: TCP, port: 53} + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: vekil-system + podSelector: + matchLabels: + app.kubernetes.io/name: vekil + ports: + - {protocol: TCP, port: 1337} diff --git a/config/provider-proxy/service.yaml b/config/provider-proxy/service.yaml new file mode 100644 index 000000000..5049006df --- /dev/null +++ b/config/provider-proxy/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: provider-auth-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy +spec: + selector: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy + ports: + - name: http + port: 8080 + targetPort: http diff --git a/config/provider-proxy/serviceaccount.yaml b/config/provider-proxy/serviceaccount.yaml new file mode 100644 index 000000000..2583872e0 --- /dev/null +++ b/config/provider-proxy/serviceaccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: provider-auth-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: provider-auth-proxy +automountServiceAccountToken: false diff --git a/config/publisher/deployment.yaml b/config/publisher/deployment.yaml new file mode 100644 index 000000000..3920d27c0 --- /dev/null +++ b/config/publisher/deployment.yaml @@ -0,0 +1,133 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workspace-publisher + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher + template: + metadata: + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher + orka.ai/network-role: workspace-publisher + spec: + serviceAccountName: workspace-publisher + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: publisher + image: docker.io/sozercan/orka-workspace-publisher:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + - name: ORKA_SCM_EGRESS_PROXY_TOKEN + valueFrom: + secretKeyRef: + name: scm-egress-proxy-auth + key: token + - name: HTTPS_PROXY + value: http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@orka-scm-egress-proxy.orka-system.svc:8080 + - name: https_proxy + value: http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@orka-scm-egress-proxy.orka-system.svc:8080 + - name: NO_PROXY + value: localhost,127.0.0.1,::1,.svc,.cluster.local + - name: no_proxy + value: localhost,127.0.0.1,::1,.svc,.cluster.local + - name: ORKA_PUBLISHER_SCM_EGRESS_PROXY_REQUIRED + value: "true" + - name: ORKA_PUBLISHER_LISTEN_ADDRESS + value: :8080 + - name: ORKA_PUBLISHER_TEMP_ROOT + value: /tmp/orka-workspace-publisher/runtime + - name: ORKA_PUBLISHER_CONTROLLER_TOKEN_FILE + value: /var/run/orka/publisher-auth/controller-token + - name: ORKA_PUBLISHER_OPERATION_CAPABILITY_SECRET_FILE + value: /var/run/orka/publisher-auth/operation-capability-secret + - name: ORKA_PUBLISHER_ARTIFACT_AUTHORIZATION_BROKER_URL + value: http://orka-api.orka-system.svc:8080 + - name: ORKA_PUBLISHER_ARTIFACT_API_URL + value: http://orka-api.orka-system.svc:8080 + - name: ORKA_PUBLISHER_CREDENTIAL_BROKER_URL + value: http://orka-api.orka-system.svc:8080 + - name: ORKA_PUBLISHER_ALLOWED_SCM_HOSTS + value: github.com + - name: ORKA_PUBLISHER_GITHUB_PR_ENABLED + value: "true" + - name: ORKA_PUBLISHER_GITHUB_API_BASE_URL + value: https://api.github.com + - name: ORKA_PUBLISHER_GITHUB_REQUEST_TIMEOUT + value: 15s + - name: ORKA_PUBLISHER_GITHUB_MAX_RESPONSE_BYTES + value: "4194304" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 100m + memory: 256Mi + ephemeral-storage: 512Mi + limits: + cpu: "2" + memory: 2Gi + ephemeral-storage: 2Gi + volumeMounts: + - name: data + mountPath: /data + - name: tmp + mountPath: /tmp/orka-workspace-publisher + - name: publisher-auth + mountPath: /var/run/orka/publisher-auth/controller-token + subPath: controller-token + readOnly: true + - name: publisher-auth + mountPath: /var/run/orka/publisher-auth/operation-capability-secret + subPath: operation-capability-secret + readOnly: true + readinessProbe: + httpGet: + path: /v1/health + port: http + periodSeconds: 5 + livenessProbe: + httpGet: + path: /v1/health + port: http + periodSeconds: 10 + volumes: + - name: data + persistentVolumeClaim: + claimName: workspace-publisher + - name: tmp + emptyDir: + sizeLimit: 1Gi + - name: publisher-auth + secret: + secretName: workspace-publisher-auth + # subPath bind mounts expose regular files to the fail-closed + # publisher loader; fsGroup grants only the Pod group read access. + defaultMode: 0440 + items: + - key: controller-token + path: controller-token + - key: operation-capability-secret + path: operation-capability-secret diff --git a/config/publisher/kustomization.yaml b/config/publisher/kustomization.yaml new file mode 100644 index 000000000..de2d4ab7d --- /dev/null +++ b/config/publisher/kustomization.yaml @@ -0,0 +1,10 @@ +resources: + - serviceaccount.yaml + - service.yaml + - pvc.yaml + - deployment.yaml + - networkpolicy.yaml +images: + - name: docker.io/sozercan/orka-workspace-publisher + newName: docker.io/sozercan/orka-workspace-publisher + newTag: latest diff --git a/config/publisher/networkpolicy.yaml b/config/publisher/networkpolicy.yaml new file mode 100644 index 000000000..bf246b9b8 --- /dev/null +++ b/config/publisher/networkpolicy.yaml @@ -0,0 +1,46 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: workspace-publisher +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher + policyTypes: [Ingress, Egress] + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: orka + control-plane: controller-manager + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - {protocol: UDP, port: 53} + - {protocol: TCP, port: 53} + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: orka + control-plane: controller-manager + ports: + - protocol: TCP + port: 8080 + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy + ports: + - protocol: TCP + port: 8080 diff --git a/config/publisher/pvc.yaml b/config/publisher/pvc.yaml new file mode 100644 index 000000000..b5f2b7c6f --- /dev/null +++ b/config/publisher/pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: workspace-publisher + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/config/publisher/service.yaml b/config/publisher/service.yaml new file mode 100644 index 000000000..b1ad8e204 --- /dev/null +++ b/config/publisher/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: workspace-publisher + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher +spec: + selector: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher + ports: + - name: http + port: 8080 + targetPort: http diff --git a/config/publisher/serviceaccount.yaml b/config/publisher/serviceaccount.yaml new file mode 100644 index 000000000..d8edf9db8 --- /dev/null +++ b/config/publisher/serviceaccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: workspace-publisher + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher +automountServiceAccountToken: false diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 654f1c85f..8cbc75951 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -8,7 +8,10 @@ rules: - "" resources: - configmaps + - namespaces + - pods - secrets + - services verbs: - create - delete @@ -21,14 +24,11 @@ rules: - "" resources: - endpoints - - namespaces - nodes - persistentvolumeclaims - persistentvolumes - - pods - pods/status - replicationcontrollers - - services verbs: - get - list @@ -89,12 +89,23 @@ rules: - apps resources: - daemonsets + - statefulsets + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: - deployments - replicasets - - statefulsets verbs: + - create + - delete - get - list + - patch + - update - watch - apiGroups: - ate.dev @@ -155,9 +166,16 @@ rules: resources: - agentruntimes - agents + - branchclaims + - controllerepochs + - externaleffects + - promptattempts - providers + - publications - repositorymonitors - repositoryscans + - runtimepools + - runtimesessioncontrols - skills - substrateactorpools - tasks @@ -175,10 +193,17 @@ rules: resources: - agentruntimes/finalizers - agents/finalizers + - branchclaims/finalizers + - controllerepochs/finalizers + - externaleffects/finalizers - outboundaccesspolicies/finalizers + - promptattempts/finalizers - providers/finalizers + - publications/finalizers - repositorymonitors/finalizers - repositoryscans/finalizers + - runtimepools/finalizers + - runtimesessioncontrols/finalizers - skills/finalizers - substrateactorpools/finalizers - tasks/finalizers @@ -190,10 +215,17 @@ rules: resources: - agentruntimes/status - agents/status + - branchclaims/status + - controllerepochs/status + - externaleffects/status - outboundaccesspolicies/status + - promptattempts/status - providers/status + - publications/status - repositorymonitors/status - repositoryscans/status + - runtimepools/status + - runtimesessioncontrols/status - skills/status - substrateactorpools/status - tasks/status @@ -303,14 +335,20 @@ rules: - delete - get - list + - patch + - update - watch - apiGroups: - policy resources: - poddisruptionbudgets verbs: + - create + - delete - get - list + - patch + - update - watch - apiGroups: - rbac.authorization.k8s.io diff --git a/config/samples/core_v1alpha1_agent.yaml b/config/samples/core_v1alpha1_agent.yaml index a147c394b..bb9531b6e 100644 --- a/config/samples/core_v1alpha1_agent.yaml +++ b/config/samples/core_v1alpha1_agent.yaml @@ -1,9 +1,20 @@ +# This Agent shows the future namespace-local external v2 selector shape from +# core_v1alpha1_agentruntime.yaml. Registration and conformance are available, +# but runtimeRef Task dispatch remains fail-closed until the external v2 +# dispatcher support boundary is enabled. Built-in Codex, Claude, and Copilot +# Agents use runtime.type instead; OpenCode is not a supported built-in type. apiVersion: core.orka.ai/v1alpha1 kind: Agent metadata: labels: app.kubernetes.io/name: orka app.kubernetes.io/managed-by: kustomize - name: agent-sample + name: external-v2-agent spec: - # TODO(user): Add fields here + model: + name: operator-reviewed-model + systemPrompt: + inline: "You are an operator-reviewed external coding assistant." + runtime: + runtimeRef: + name: sample-external-v2-runtime diff --git a/config/samples/core_v1alpha1_agent_claude.yaml b/config/samples/core_v1alpha1_agent_claude.yaml index 520c7129e..4e46a279e 100644 --- a/config/samples/core_v1alpha1_agent_claude.yaml +++ b/config/samples/core_v1alpha1_agent_claude.yaml @@ -12,13 +12,14 @@ spec: # systemPrompt is injected via --system-prompt; can also use configMapRef systemPrompt: inline: "You are a helpful coding assistant." - # secretRef references a Secret containing ANTHROPIC_API_KEY - secretRef: - name: claude-api-key - # runtime marks this Agent for type: agent tasks using Claude Code CLI - # (mutually exclusive with providerRef, which is for type: ai tasks) + # Provider credentials are injected only by the central authenticated proxy; + # no provider Secret is referenced by an ACP Agent or delivered to its runtime. + # runtime.type selects the built-in Claude ACP profile backed exclusively by + # a controller-owned, digest-pinned orka.harness.v2 RuntimePool. There is no + # per-Task Job/image or legacy harness fallback. It is mutually exclusive + # with providerRef, which is for native type: ai tasks. runtime: - # type selects the CLI runtime: "claude", "copilot", "codex", or "opencode" + # type selects a supported ACP runtime profile. type: claude # defaultMaxTurns is the default max agent loop iterations per task defaultMaxTurns: 50 @@ -32,13 +33,3 @@ spec: - Bash - Glob - Grep - # execution sets default runtime isolation and placement for worker pods - # execution: - # runtimeClassName: gvisor - # nodeSelector: - # sandbox-runtime: gvisor - # tolerations: - # - key: sandbox-runtime - # operator: Equal - # value: gvisor - # effect: NoSchedule diff --git a/config/samples/core_v1alpha1_agent_codex.yaml b/config/samples/core_v1alpha1_agent_codex.yaml index 3f4a36490..58167f277 100644 --- a/config/samples/core_v1alpha1_agent_codex.yaml +++ b/config/samples/core_v1alpha1_agent_codex.yaml @@ -10,8 +10,11 @@ spec: name: "gpt-5.4" systemPrompt: inline: "You are a helpful coding assistant." - secretRef: - name: codex-api-key + # Provider credentials are injected only by the central authenticated proxy; + # no provider Secret is referenced by an ACP Agent or delivered to its runtime. + # runtime.type selects the built-in Codex ACP profile backed exclusively by + # a controller-owned, digest-pinned orka.harness.v2 RuntimePool. There is no + # per-Task Job/image or legacy harness fallback. runtime: type: codex defaultMaxTurns: 50 diff --git a/config/samples/core_v1alpha1_agent_opencode.yaml b/config/samples/core_v1alpha1_agent_opencode.yaml deleted file mode 100644 index 4bd0ef25e..000000000 --- a/config/samples/core_v1alpha1_agent_opencode.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: Agent -metadata: - name: opencode-agent - labels: - app.kubernetes.io/name: orka - app.kubernetes.io/managed-by: kustomize -spec: - model: - name: "kimi-k2" - systemPrompt: - inline: "You are a helpful coding assistant." - # The Secret must contain OPENAI_BASE_URL. Add OPENAI_API_KEY when authentication is required. - secretRef: - name: opencode-credentials - runtime: - type: opencode - defaultMaxTurns: 50 - defaultAllowBash: true diff --git a/config/samples/core_v1alpha1_agentruntime.yaml b/config/samples/core_v1alpha1_agentruntime.yaml index a0b4fd885..8df079fee 100644 --- a/config/samples/core_v1alpha1_agentruntime.yaml +++ b/config/samples/core_v1alpha1_agentruntime.yaml @@ -1,26 +1,75 @@ +# External runtimes are operator-owned orka.harness.v2 services. Registration +# and strict workspace-governance conformance are available, but runtimeRef Task +# dispatch remains fail-closed until the external v2 dispatcher boundary is +# enabled. Prefer controller-owned RuntimePools for built-in Codex, Claude, and +# Copilot profiles. +# +# Before applying this object, create both referenced Secrets in this namespace. +# Each Secret must contain at least 32 bytes, set +# orka.ai/agent-runtime-auth: "true" +# and bind itself to this runtime and endpoint with +# orka.ai/agent-runtime-name: sample-external-v2-runtime +# orka.ai/agent-runtime-endpoint: http://sample-external-v2-runtime.default.svc.cluster.local:8080 +# The capability/profile values below are illustrative. They must exactly match +# the runtime's public /v2/capabilities and authenticated /v2/status responses. apiVersion: core.orka.ai/v1alpha1 kind: AgentRuntime metadata: labels: app.kubernetes.io/name: orka app.kubernetes.io/managed-by: kustomize - name: sample-http-runtime + name: sample-external-v2-runtime spec: - # Namespace-local AgentRuntime facade for a remote execution backend. - # The endpoint may point at a generic HTTP runtime, AgentKit Serve adapter, - # Foundry adapter, or another backend implementing orka.harness.v1. - # The bearer token authenticates Orka to the runtime endpoint; it is not a - # downstream production tool credential. - contractVersion: orka.harness.v1 + contractVersion: orka.harness.v2 deployment: mode: external-endpoint - endpoint: http://sample-http-runtime.default.svc.cluster.local:8080 + endpoint: http://sample-external-v2-runtime.default.svc.cluster.local:8080 clientAuth: - bearerTokenSecretRef: - name: sample-http-runtime-token + controllerBearerTokenSecretRef: + name: sample-external-v2-runtime-controller-auth key: token + operationCapabilitySecretRef: + name: sample-external-v2-runtime-operation-auth + key: capability-secret capabilities: - toolExecutionModes: - - observed - supportsCancel: true - supportsRuntimeSessions: true + runtimeInstanceID: sample-external-v2-runtime-01 + profile: + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + digestSchemaVersion: 1 + acpProfile: acp.v1 + adapterName: operator-reviewed-adapter + adapterDigest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + providerKind: operator-managed + model: operator-reviewed-model + agentConfigurationDigest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + toolPolicyDigest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + approvalPolicyDigest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + mcpConfigurationDigest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + workspaceIntent: read + proxyCredentialRole: operator-managed + proxyCredentialScope: external-runtime + resourceClass: external + limits: + maxResidentSessions: 10 + maxConcurrentPrompts: 4 + maxRequestBytes: 1048576 + maxEventLineBytes: 262144 + maxTerminalResultBytes: 1048576 + maxBufferedEvents: 4096 + maxUpdateEventsPerSecond: 100 + minPromptLeaseMillis: 5000 + maxPromptLeaseMillis: 120000 + maxPendingPermissions: 32 + maxWorkspaceDeltaBytes: 104857600 + supportsDrain: false + supportsPublicationFinalization: false + workspaceGovernance: + mode: strict-governed + trusted: false + orkaOwnedWorkspaceDeltas: true + promptScopedBrokerAuthorization: true + noDirectSCMPublication: true + orkaOwnedCleanRoomPublication: true + exactInstanceFencing: true + duplicateSafeMutations: true + cancellationSettlement: true diff --git a/config/samples/core_v1alpha1_agentruntime_agentkit.yaml b/config/samples/core_v1alpha1_agentruntime_agentkit.yaml deleted file mode 100644 index 7ffc602c6..000000000 --- a/config/samples/core_v1alpha1_agentruntime_agentkit.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: AgentRuntime -metadata: - labels: - app.kubernetes.io/name: orka - app.kubernetes.io/managed-by: kustomize - name: sample-agentkit-runtime -spec: - # Namespace-local facade for an operator-deployed AgentKit Serve adapter. - # Current AgentKit Serve Orka support is observed mode only; do not add brokered - # classes or supportsContinuation until AgentKit passes those conformance profiles. - # Adapter/runtime credentials live in the adapter deployment, not in Orka Tool - # credentials or the remote workload. - contractVersion: orka.harness.v1 - deployment: - mode: external-endpoint - endpoint: http://sample-agentkit-runtime.default.svc.cluster.local:8080 - clientAuth: - bearerTokenSecretRef: - name: sample-agentkit-runtime-token - key: token - capabilities: - toolExecutionModes: - - observed - supportsCancel: true - supportsRuntimeSessions: true diff --git a/config/samples/core_v1alpha1_agentruntime_foundry.yaml b/config/samples/core_v1alpha1_agentruntime_foundry.yaml deleted file mode 100644 index f98f1fe32..000000000 --- a/config/samples/core_v1alpha1_agentruntime_foundry.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: AgentRuntime -metadata: - labels: - app.kubernetes.io/name: orka - app.kubernetes.io/managed-by: kustomize - name: sample-foundry-runtime -spec: - # Namespace-local facade for the separately deployed orka-agents/agent-runtime-foundry - # Hosted Agents Responses adapter. Azure authentication belongs to the adapter deployment - # (for example, Workload Identity). Orka Tool credentials remain Orka-governed. - contractVersion: orka.harness.v1 - deployment: - mode: external-endpoint - endpoint: http://sample-foundry-runtime.default.svc.cluster.local:8080 - clientAuth: - bearerTokenSecretRef: - name: sample-foundry-runtime-token - key: token - capabilities: - # The adapter is observed-only by default. Advertise brokered classes only after - # enabling ORKA_FOUNDRY_BROKERED_TOOL_CLASSES and passing matching conformance probes. - toolExecutionModes: - - observed - supportsCancel: true - supportsRuntimeSessions: true diff --git a/config/samples/core_v1alpha1_task_agent.yaml b/config/samples/core_v1alpha1_task_agent.yaml index 11d93db23..78894fdfa 100644 --- a/config/samples/core_v1alpha1_task_agent.yaml +++ b/config/samples/core_v1alpha1_task_agent.yaml @@ -6,35 +6,23 @@ metadata: app.kubernetes.io/name: orka app.kubernetes.io/managed-by: kustomize spec: - # type: agent delegates work to an external CLI runtime (e.g., Claude Code CLI or Codex CLI) + # type: agent uses a fenced ACP v2 RuntimeSession in a controller-owned pool. + # Provider authentication comes only through the central provider proxy; + # neither this Task nor the Agent references a provider credential Secret. type: agent - # agentRef references an Agent CRD that defines runtime type and credentials agentRef: name: claude-agent - # execution optionally overrides runtime isolation and placement for this task - # execution: - # runtimeClassName: gvisor - # nodeSelector: - # sandbox-runtime: gvisor - # tolerations: - # - key: sandbox-runtime - # operator: Equal - # value: gvisor - # effect: NoSchedule - # prompt is the instruction sent to the agent CLI - prompt: "Refactor the main.go file to use structured logging" - # agentRuntime allows task-level overrides of the Agent's runtime defaults + prompt: "Review main.go for structured logging opportunities. Do not modify files." + workspace: + intent: read + gitRepo: "https://github.com/example/my-project.git" + branch: "main" + # Optional for private repositories; used only by the clean-room clone path. + # readCredentialRef: + # name: my-project-read + # agentRuntime contains only task-level overrides of Agent runtime defaults. agentRuntime: - workspace: - # gitRepo is the repository URL to clone into the agent's workspace - gitRepo: "https://github.com/example/my-project.git" - # branch to checkout - branch: "main" - # maxTurns limits agent loop iterations (overrides Agent's defaultMaxTurns) maxTurns: 100 - # allowBash enables bash command execution (overrides Agent's defaultAllowBash) allowBash: true - # timeout is the maximum duration before the task is terminated timeout: "30m" - # priority controls queue ordering (0-1000, higher = more urgent) priority: 500 diff --git a/config/samples/core_v1alpha1_task_agent_copilot.yaml b/config/samples/core_v1alpha1_task_agent_copilot.yaml deleted file mode 100644 index a7750f50b..000000000 --- a/config/samples/core_v1alpha1_task_agent_copilot.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: Task -metadata: - name: agent-task-copilot-example - labels: - app.kubernetes.io/name: orka - app.kubernetes.io/managed-by: kustomize -spec: - # type: agent delegates work to an external CLI runtime - type: agent - # Reference an Agent configured with runtime.type: copilot - agentRef: - name: copilot-agent - # Prompt sent to the Copilot CLI agent - prompt: "Add unit tests for the authentication middleware" - # Task-level runtime overrides (optional) - agentRuntime: - maxTurns: 30 - # Maximum time before the task is terminated - timeout: "20m" - priority: 500 diff --git a/config/samples/core_v1alpha1_task_agent_workspace.yaml b/config/samples/core_v1alpha1_task_agent_workspace.yaml index c523f027b..76aa27070 100644 --- a/config/samples/core_v1alpha1_task_agent_workspace.yaml +++ b/config/samples/core_v1alpha1_task_agent_workspace.yaml @@ -6,30 +6,42 @@ metadata: app.kubernetes.io/name: orka app.kubernetes.io/managed-by: kustomize spec: - # type: agent delegates work to an external CLI runtime + # type: agent uses the ACP v2 RuntimePool selected from the referenced Agent. type: agent - # Reference an Agent with runtime configured (e.g., claude-agent) agentRef: name: claude-agent - # Prompt for the agent to execute in the cloned workspace prompt: "Fix the failing CI tests in the api/ directory" - # Agent runtime overrides with workspace git clone configuration + # Repository input and delivery policy live at top-level spec.workspace. + workspace: + intent: write + # The clean-room workspace boundary clones this credential-free source URL. + gitRepo: "https://github.com/example/my-project.git" + branch: "main" + # Use ref instead of branch to pin a commit or tag. + # ref: "0123456789abcdef0123456789abcdef01234567" + readCredentialRef: + name: my-project-source-read + # Publication is a separate repository/credential role. The credential is + # resolved only by the Workspace/Publisher and never enters the ACP runtime. + publicationGitRepo: "https://github.com/example/my-project.git" + publicationReadCredentialRef: + name: my-project-publication-read + key: token + publicationCredentialRef: + name: my-project-publication-write + key: token + forgeCredentialRef: + name: my-project-forge + key: token + pushBranch: "orka/fix-api-tests" + prBaseBranch: "main" + createPR: true + # subPath restricts the workspace root to a repository subdirectory. + # subPath: "services/api" + # agentRuntime contains only per-Task runtime overrides. agentRuntime: - workspace: - # gitRepo is the repository URL to clone into the workspace - gitRepo: "https://github.com/example/my-project.git" - # branch to checkout (optional, defaults to repo default branch) - branch: "feature/fix-tests" - # ref can be a specific commit SHA or tag (optional, mutually exclusive with branch) - # ref: "abc123" - # gitSecretRef references a Secret with git credentials for private repos - gitSecretRef: - name: git-credentials - # subPath restricts the workspace root to a subdirectory of the repo - # subPath: "services/api" maxTurns: 100 allowBash: true - # allowedTools overrides the Agent's defaultAllowedTools for this task allowedTools: - Read - Write diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 1a35ce3a4..6b9a0994e 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -2,16 +2,12 @@ resources: - core_v1alpha1_task.yaml - core_v1alpha1_task_agent.yaml -- core_v1alpha1_task_agent_copilot.yaml - core_v1alpha1_task_agent_workspace.yaml - core_v1alpha1_tool.yaml - core_v1alpha1_agent.yaml - core_v1alpha1_agentruntime.yaml -- core_v1alpha1_agentruntime_agentkit.yaml -- core_v1alpha1_agentruntime_foundry.yaml - core_v1alpha1_agent_claude.yaml - core_v1alpha1_agent_codex.yaml -- core_v1alpha1_agent_opencode.yaml - gateway_v1alpha1_gatewayclass.yaml - gateway_v1alpha1_gateway.yaml - gateway_v1alpha1_gatewaybinding.yaml @@ -21,4 +17,5 @@ resources: - workspace_v1alpha1_executionworkspace.yaml - fake.workspace_v1alpha1_fakeproviderconfig.yaml - fake.workspace_v1alpha1_fakepoolparameters.yaml + # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/scm-egress-proxy/README.md b/config/scm-egress-proxy/README.md new file mode 100644 index 000000000..08a1bc571 --- /dev/null +++ b/config/scm-egress-proxy/README.md @@ -0,0 +1,21 @@ +# SCM egress proxy + +This package is included by `config/default`. Before applying it, create the +shared Publisher/proxy authentication Secret in `orka-system`: + +```bash +token="$(openssl rand -hex 32)" +kubectl -n orka-system create secret generic scm-egress-proxy-auth \ + --from-literal=token="$token" +unset token +``` + +The token must contain 32-256 RFC 3986 unreserved characters (`A-Z`, `a-z`, +`0-9`, `-`, `.`, `_`, or `~`). It authenticates the Publisher to the proxy; it +is not an SCM credential. + +`deployment.yaml` allows only `github.com` plus `api.github.com`. Patch +`--allowed-hosts` and `--forge-api-base-url` together with the Publisher's +`ORKA_PUBLISHER_ALLOWED_SCM_HOSTS` and forge API URL when using GitHub +Enterprise or another reviewed forge endpoint. Hostnames are exact and +lower-case; wildcards and IP literals are rejected. diff --git a/config/scm-egress-proxy/deployment.yaml b/config/scm-egress-proxy/deployment.yaml new file mode 100644 index 000000000..c153bd346 --- /dev/null +++ b/config/scm-egress-proxy/deployment.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scm-egress-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy + template: + metadata: + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy + orka.ai/network-role: scm-egress-proxy + spec: + serviceAccountName: scm-egress-proxy + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: proxy + image: controller:latest + imagePullPolicy: IfNotPresent + command: [/scm-egress-proxy] + args: + - --listen-address=:8080 + - --allowed-hosts=github.com + - --forge-api-base-url=https://api.github.com + - --token-file=/var/run/secrets/orka/scm-egress/token + - --max-request-header-bytes=32768 + - --max-response-header-bytes=65536 + - --max-request-bytes=4194304 + - --max-response-bytes=8388608 + - --max-tunnel-bytes=1073741824 + - --max-concurrent=8 + - --resolution-timeout=5s + - --connect-timeout=10s + - --response-header-timeout=30s + - --forward-timeout=2m + - --idle-timeout=30s + - --tunnel-timeout=10m + ports: + - name: http-proxy + containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 25m + memory: 32Mi + ephemeral-storage: 32Mi + limits: + cpu: 500m + memory: 256Mi + ephemeral-storage: 128Mi + readinessProbe: + httpGet: + path: /readyz + port: http-proxy + livenessProbe: + httpGet: + path: /healthz + port: http-proxy + volumeMounts: + - name: auth + mountPath: /var/run/secrets/orka/scm-egress/token + subPath: token + readOnly: true + volumes: + - name: auth + secret: + secretName: scm-egress-proxy-auth + defaultMode: 0440 + items: + - key: token + path: token diff --git a/config/scm-egress-proxy/kustomization.yaml b/config/scm-egress-proxy/kustomization.yaml new file mode 100644 index 000000000..47d648cb4 --- /dev/null +++ b/config/scm-egress-proxy/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - serviceaccount.yaml + - service.yaml + - deployment.yaml + - networkpolicy.yaml +images: + - name: controller + newName: ghcr.io/orka-agents/orka + newTag: latest diff --git a/config/scm-egress-proxy/networkpolicy.yaml b/config/scm-egress-proxy/networkpolicy.yaml new file mode 100644 index 000000000..8a0ae6671 --- /dev/null +++ b/config/scm-egress-proxy/networkpolicy.yaml @@ -0,0 +1,70 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: scm-egress-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy + policyTypes: [Ingress, Egress] + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: workspace-publisher + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - {protocol: UDP, port: 53} + - {protocol: TCP, port: 53} + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 0.0.0.0/8 + - 10.0.0.0/8 + - 100.64.0.0/10 + - 127.0.0.0/8 + - 169.254.0.0/16 + - 172.16.0.0/12 + - 192.0.0.0/24 + - 192.0.2.0/24 + - 192.168.0.0/16 + - 198.18.0.0/15 + - 198.51.100.0/24 + - 203.0.113.0/24 + - 224.0.0.0/4 + - 240.0.0.0/4 + ports: + - {protocol: TCP, port: 443} + - to: + - ipBlock: + cidr: ::/0 + except: + - ::/128 + - ::1/128 + - 64:ff9b::/96 + - 64:ff9b:1::/48 + - 100::/64 + - 2001::/32 + - 2001:db8::/32 + - 2002::/16 + - fc00::/7 + - fe80::/10 + - ff00::/8 + ports: + - {protocol: TCP, port: 443} diff --git a/config/scm-egress-proxy/service.yaml b/config/scm-egress-proxy/service.yaml new file mode 100644 index 000000000..e794bb329 --- /dev/null +++ b/config/scm-egress-proxy/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: scm-egress-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy +spec: + selector: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy + ports: + - name: http-proxy + port: 8080 + targetPort: http-proxy diff --git a/config/scm-egress-proxy/serviceaccount.yaml b/config/scm-egress-proxy/serviceaccount.yaml new file mode 100644 index 000000000..744c2bafc --- /dev/null +++ b/config/scm-egress-proxy/serviceaccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: scm-egress-proxy + labels: + app.kubernetes.io/name: orka + app.kubernetes.io/component: scm-egress-proxy +automountServiceAccountToken: false diff --git a/config/vekil-ingress/kustomization.yaml b/config/vekil-ingress/kustomization.yaml new file mode 100644 index 000000000..b90ff79ed --- /dev/null +++ b/config/vekil-ingress/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - networkpolicy.yaml diff --git a/config/vekil-ingress/networkpolicy.yaml b/config/vekil-ingress/networkpolicy.yaml new file mode 100644 index 000000000..b77cf4896 --- /dev/null +++ b/config/vekil-ingress/networkpolicy.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: orka-provider-auth-proxy-only + namespace: vekil-system + labels: + app.kubernetes.io/managed-by: orka +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: vekil + policyTypes: [Ingress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: orka-system + podSelector: + matchLabels: + orka.ai/network-role: provider-auth-proxy + ports: + - protocol: TCP + port: 1337 diff --git a/deploy/orka.yaml b/deploy/orka.yaml index c955a68da..459f39f55 100644 --- a/deploy/orka.yaml +++ b/deploy/orka.yaml @@ -10,6755 +10,61 @@ metadata: pod-security.kubernetes.io/warn: restricted name: orka-system --- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: agentruntimes.core.orka.ai -spec: - group: core.orka.ai - names: - kind: AgentRuntime - listKind: AgentRuntimeList - plural: agentruntimes - singular: agentruntime - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.ready - name: Ready - type: boolean - - jsonPath: .spec.contractVersion - name: Contract - type: string - - jsonPath: .spec.deployment.mode - name: Mode - type: string - - jsonPath: .status.observedCapabilities.runtimeName - name: Runtime - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: AgentRuntime is the Schema for registered Orka harness runtimes. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: AgentRuntimeRegistrySpec defines the desired state of a registered - Orka harness runtime. - properties: - capabilities: - description: Capabilities declares readiness requirements Orka checks - against the runtime. - properties: - brokeredToolClasses: - description: BrokeredToolClasses lists brokered tool classes the - runtime must advertise when brokered mode is required. - items: - description: AgentRuntimeBrokeredToolClass declares which classes - of Orka-brokered tools a runtime can request. - enum: - - read - - write - - coordination - type: string - type: array - x-kubernetes-list-type: set - supportsArtifacts: - description: SupportsArtifacts requires the runtime to advertise - artifact/result reference support when true. - type: boolean - supportsCancel: - description: SupportsCancel requires the runtime to advertise - cancellation support when true. - type: boolean - supportsContinuation: - description: SupportsContinuation requires the runtime to advertise - continuation after Orka-brokered tool results when true. - type: boolean - supportsRuntimeSessions: - description: SupportsRuntimeSessions requires the runtime to advertise - stable runtime sessions when true. - type: boolean - toolExecutionModes: - description: ToolExecutionModes lists tool execution modes the - runtime must advertise. - items: - description: AgentRuntimeToolExecutionMode declares how custom - runtimes interact with tools. - enum: - - observed - - brokered - type: string - type: array - x-kubernetes-list-type: set - type: object - clientAuth: - description: ClientAuth configures controller-to-runtime authentication. - properties: - bearerTokenSecretRef: - description: |- - BearerAuthRef points to the bearer token Secret used for mutating harness endpoints. - The referenced Secret must opt in with label orka.ai/agent-runtime-auth=true, - may set orka.ai/agent-runtime-name= to restrict use to one AgentRuntime, - and must set annotation orka.ai/agent-runtime-endpoint= to bind the token to one endpoint. - properties: - key: - description: Key is the Secret data key containing the bearer - token. - minLength: 1 - type: string - name: - description: Name is the Secret name. - minLength: 1 - type: string - required: - - key - - name - type: object - required: - - bearerTokenSecretRef - type: object - contractVersion: - default: orka.harness.v1 - description: ContractVersion is the Orka harness contract this runtime - must implement. - enum: - - orka.harness.v1 - type: string - deployment: - description: Deployment identifies the runtime endpoint provider. - properties: - endpoint: - description: |- - Endpoint is the base URL for a pre-deployed or external orka.harness.v1 service. - It must not contain credentials; bearer auth is configured via clientAuth. - pattern: ^https?://[^\s@?#]+$ - type: string - mode: - description: Mode is the deployment mode. The first milestone - supports external endpoints only. - enum: - - external-endpoint - type: string - required: - - endpoint - - mode - type: object - required: - - clientAuth - - contractVersion - - deployment - type: object - status: - description: AgentRuntimeStatus defines the observed state of an AgentRuntime. - properties: - conditions: - description: Conditions represent the current state of the AgentRuntime. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - lastValidated: - description: LastValidated is the timestamp of the last readiness - probe. - format: date-time - type: string - message: - description: Message provides sanitized readiness context. - type: string - observedAuthRefResourceVersion: - description: |- - ObservedAuthRefResourceVersion is the resourceVersion of the bearer auth Secret - used for the last readiness probe. It is non-secret metadata used to decide - when token rotation requires a fresh authenticated conformance turn. - type: string - observedCapabilities: - description: ObservedCapabilities contains sanitized capabilities - from the last probe. - properties: - brokeredToolClasses: - description: BrokeredToolClasses are the brokered tool classes - advertised by /v1/capabilities. - items: - description: AgentRuntimeBrokeredToolClass declares which classes - of Orka-brokered tools a runtime can request. - enum: - - read - - write - - coordination - type: string - type: array - x-kubernetes-list-type: set - maxConcurrentTurns: - description: MaxConcurrentTurns is the advertised concurrency - ceiling. - type: integer - maxOutputBytes: - description: MaxOutputBytes is the advertised maximum output payload - size. - format: int64 - type: integer - maxTurnSeconds: - description: MaxTurnSeconds is the advertised per-turn duration - ceiling. - type: integer - protocolVersion: - description: ProtocolVersion is the runtime's advertised Orka - protocol version. - type: string - providerKind: - description: ProviderKind is the provider kind advertised by /v1/capabilities. - type: string - runtimeName: - description: RuntimeName is the runtime name advertised by /v1/capabilities. - type: string - runtimeVersion: - description: RuntimeVersion is the runtime version advertised - by /v1/capabilities. - type: string - supportsArtifacts: - description: SupportsArtifacts reports whether the runtime advertises - artifact/result reference support. - type: boolean - supportsCancel: - description: SupportsCancel reports whether the runtime advertises - cancellation support. - type: boolean - supportsContinuation: - description: SupportsContinuation reports whether the runtime - advertises continuation support. - type: boolean - supportsRuntimeSessions: - description: SupportsRuntimeSessions reports whether the runtime - advertises runtime-session support. - type: boolean - supportsSuspend: - description: SupportsSuspend reports whether the runtime advertises - suspend support. - type: boolean - supportsWorkspaceSnapshot: - description: SupportsWorkspaceSnapshot reports whether the runtime - advertises workspace snapshots. - type: boolean - toolExecutionModes: - description: ToolExecutionModes are the tool modes advertised - by /v1/capabilities. - items: - description: AgentRuntimeToolExecutionMode declares how custom - runtimes interact with tools. - enum: - - observed - - brokered - type: string - type: array - x-kubernetes-list-type: set - transport: - description: Transport is the runtime transport, normally http+sse. - type: string - type: object - observedGeneration: - description: ObservedGeneration is the latest generation reconciled - into this status. - format: int64 - type: integer - ready: - description: Ready indicates the runtime passed the configured Orka - readiness checks. - type: boolean - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: agents.core.orka.ai -spec: - group: core.orka.ai - names: - kind: Agent - listKind: AgentList - plural: agents - singular: agent - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.model.provider - name: Provider - type: string - - jsonPath: .spec.model.name - name: Model - type: string - - jsonPath: .status.ready - name: Ready - type: boolean - - jsonPath: .status.activeTasks - name: Active - type: integer - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: Agent is the Schema for the agents API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: AgentSpec defines the desired state of Agent - properties: - coordination: - description: Coordination enables agent-to-agent delegation - properties: - allowedAgents: - description: AllowedAgents lists agents this agent can delegate - to - items: - description: AllowedAgent defines an agent that can be delegated - to - properties: - name: - description: Name is the name of the agent - type: string - namespace: - description: Namespace is the namespace of the agent (defaults - to same namespace) - type: string - required: - - name - type: object - type: array - approvalRequiredTools: - description: |- - ApprovalRequiredTools lists custom tool names that require a human approval before execution. - This field is only honored when coordination is enabled and autonomous mode is true. - Built-in tools such as request_approval, delegate_task, and web_search are rejected. - items: - type: string - type: array - autonomous: - description: |- - Autonomous enables autonomous loop mode for coordinator agents using this config. - When enabled, the controller re-creates Jobs in a loop instead of marking the task as Succeeded. - type: boolean - enabled: - default: false - description: Enabled indicates if coordination is enabled - type: boolean - maxConcurrentChildren: - default: 5 - description: MaxConcurrentChildren limits concurrent child tasks - format: int32 - minimum: 1 - type: integer - maxDepth: - default: 3 - description: MaxDepth limits delegation depth to prevent infinite - loops - format: int32 - maximum: 10 - minimum: 1 - type: integer - maxIterations: - description: |- - MaxIterations limits the number of autonomous loop iterations (0 = unlimited). - Only used when Autonomous is true. - format: int32 - minimum: 0 - type: integer - required: - - enabled - type: object - execution: - description: Execution defines default worker pod runtime and placement - settings. - properties: - affinity: - description: Affinity defines Kubernetes affinity and anti-affinity - rules for worker pods. - properties: - nodeAffinity: - description: Describes node affinity scheduling rules for - the pod. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, etc.), - compute a sum by iterating through the elements of this field and adding - "weight" to the sum if the node matches the corresponding matchExpressions; the - node(s) with the highest sum are the most preferred. - items: - description: |- - An empty preferred scheduling term matches all objects with implicit weight 0 - (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). - properties: - preference: - description: A node selector term, associated with - the corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - type: object - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the - corresponding nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from its node. - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: |- - A null or empty node selector term matches no objects. The requirements of - them are ANDed. - The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - type: object - x-kubernetes-map-type: atomic - type: array - x-kubernetes-list-type: atomic - required: - - nodeSelectorTerms - type: object - x-kubernetes-map-type: atomic - type: object - podAffinity: - description: Describes pod affinity scheduling rules (e.g. - co-locate this pod in the same node, zone, etc. as some - other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, etc.), - compute a sum by iterating through the elements of this field and adding - "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: |- - weight associated with matching the corresponding podAffinityTerm, - in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod label update), the - system may or may not try to eventually evict the pod from its node. - When there are multiple elements, the lists of nodes corresponding to each - podAffinityTerm are intersected, i.e. all terms must be satisfied. - items: - description: |- - Defines a set of pods (namely those matching the labelSelector - relative to the given namespace(s)) that this pod should be - co-located (affinity) or not co-located (anti-affinity) with, - where co-located is defined as running on a node whose value of - the label with key matches that of any node on which - a pod of the set of pods is running - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - x-kubernetes-list-type: atomic - type: object - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules - (e.g. avoid putting this pod in the same node, zone, etc. - as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the anti-affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling anti-affinity expressions, etc.), - compute a sum by iterating through the elements of this field and subtracting - "weight" from the sum if the node has pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: |- - weight associated with matching the corresponding podAffinityTerm, - in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the anti-affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the anti-affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod label update), the - system may or may not try to eventually evict the pod from its node. - When there are multiple elements, the lists of nodes corresponding to each - podAffinityTerm are intersected, i.e. all terms must be satisfied. - items: - description: |- - Defines a set of pods (namely those matching the labelSelector - relative to the given namespace(s)) that this pod should be - co-located (affinity) or not co-located (anti-affinity) with, - where co-located is defined as running on a node whose value of - the label with key matches that of any node on which - a pod of the set of pods is running - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - x-kubernetes-list-type: atomic - type: object - type: object - nodeSelector: - additionalProperties: - type: string - description: NodeSelector constrains worker pods to nodes with - matching labels. - type: object - runtimeClassName: - description: RuntimeClassName routes worker pods through a specific - RuntimeClass. - type: string - tolerations: - description: Tolerations allows worker pods to schedule onto tainted - nodes. - items: - description: |- - The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . - properties: - effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. - type: string - operator: - description: |- - Operator represents a key's relationship to the value. - Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. - Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). - type: string - tolerationSeconds: - description: |- - TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). Zero and - negative values will be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. - type: string - type: object - type: array - workspace: - description: |- - Workspace requests an upstream agent-sandbox execution workspace for agent Tasks. - When enabled, the Task controller validates the request and propagates the - resolved sandbox settings to the agent worker Job. The worker wrapper then - claims the sandbox workspace and runs the configured agent runtime inside it. - properties: - boot: - description: |- - Boot asks providers that support it to boot the workspace workload from scratch - instead of resuming from the provider's default snapshot. Currently supported - by the Substrate provider. - type: boolean - cleanupPolicy: - description: |- - CleanupPolicy controls whether the workspace is deleted or retained after use. - Defaults to delete when omitted. - enum: - - delete - - retain - type: string - enabled: - default: false - description: Enabled requests use of a durable workspace for - the task execution. - type: boolean - hibernation: - description: |- - Hibernation configures process lifetime inside the workspace. Resident - mode is currently rejected until the worker protocol can report per-turn - completion separately from resident process lifetime. - properties: - processMode: - description: |- - ProcessMode controls whether each turn starts fresh or reuses a resident - process. Defaults to fresh. - enum: - - fresh - - resident - type: string - residentKey: - description: |- - ResidentKey selects the process slot for resident mode. When omitted, the - worker derives a stable key from namespace, template, and reuse key. - type: string - type: object - poolRef: - description: |- - PoolRef references an operator-managed Substrate actor pool for placement, - density tracking, and oversubscription policy. - properties: - name: - description: Name is the pool name. - type: string - namespace: - description: Namespace is the pool namespace. It defaults - to the Task namespace. - type: string - type: object - provider: - description: |- - Provider selects the workspace backend. When omitted, the controller - resolves the configured default workspace provider; the built-in - compatibility default is agent-sandbox. - enum: - - agent-sandbox - - substrate - type: string - reusePolicy: - default: none - description: |- - ReusePolicy controls whether the workspace is fresh or session-scoped. - Defaults to none when omitted. - enum: - - none - - session - type: string - snapshot: - description: |- - Snapshot configures explicit provider snapshot restore/checkpoint behavior. - Non-empty settings are currently rejected until provider checkpoint/restore - support is available through Orka. - properties: - checkpointOnRelease: - description: |- - CheckpointOnRelease requests a checkpoint when cleanup releases or retains - the workspace. CheckpointURI must be set when this is true. - type: boolean - checkpointURI: - description: |- - CheckpointURI is a provider-native snapshot URI prefix to write when the - workspace is retained or released. - type: string - restoreURI: - description: |- - RestoreURI is a provider-native snapshot URI prefix to restore before the - workspace command runs. - type: string - type: object - templateRef: - description: |- - TemplateRef references the workspace template to instantiate or reuse. - The template name is required when enabled is true unless the controller - is configured with a provider-specific default template. - properties: - name: - description: Name is the name of the workspace template. - type: string - namespace: - description: |- - Namespace is the namespace of the workspace template and claim. - It defaults to the Task namespace, or the controller namespace when configured. - type: string - type: object - type: object - type: object - model: - description: |- - Model defines the LLM model configuration - Provider field is optional if providerRef is set - properties: - fallbacks: - description: |- - Fallbacks defines alternative providers to try when the primary fails. - Each fallback specifies a Provider CRD and optional model override. - items: - description: ModelFallback defines a fallback provider configuration - properties: - model: - description: Model to use with this provider (optional, - uses provider's defaultModel if empty) - type: string - providerRef: - description: ProviderRef is the name of a Provider CRD to - fall back to - type: string - required: - - providerRef - type: object - type: array - maxTokens: - description: MaxTokens limits the response length - format: int32 - type: integer - name: - description: |- - Name is the model identifier - Optional if providerRef is set and Provider has defaultModel - type: string - provider: - description: |- - Provider is the LLM provider (anthropic, openai) - Optional if providerRef is set on the Agent - enum: - - anthropic - - openai - type: string - temperature: - default: 0.7 - description: Temperature controls randomness in generation - maximum: 2 - minimum: 0 - type: number - type: object - providerRef: - description: |- - ProviderRef references a Provider CRD for LLM configuration - If set, model.provider is optional (inherited from Provider) - properties: - name: - description: Name is the name of the Provider - type: string - namespace: - description: Namespace is the namespace of the Provider (defaults - to Task namespace) - type: string - required: - - name - type: object - rateLimit: - description: RateLimit defines rate limiting configuration - properties: - requestsPerMinute: - description: RequestsPerMinute limits requests per minute - format: int32 - type: integer - tokensPerMinute: - description: TokensPerMinute limits tokens per minute - format: int64 - type: integer - type: object - resources: - description: Resources defines the resource limits for tasks using - this agent - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - runtime: - description: |- - Runtime configures this Agent for external CLI runtimes (type: agent tasks). - When set, this Agent is for type: agent tasks only (mutually exclusive with providerRef). - properties: - defaultAllowBash: - description: |- - DefaultAllowBash controls whether bash is allowed by default for tasks using this Agent. - Defaults to true if not specified. - type: boolean - defaultAllowedTools: - description: DefaultAllowedTools lists the default tools allowed - for tasks using this Agent - items: - type: string - type: array - defaultMaxTurns: - default: 50 - description: DefaultMaxTurns is the default maximum agent loop - iterations for tasks using this Agent - format: int32 - maximum: 1000 - minimum: 1 - type: integer - runtimeRef: - description: RuntimeRef selects an admin-governed AgentRuntime - for custom/BYO harness runtimes. - properties: - name: - description: Name is the AgentRuntime name. RuntimeRefs are - resolved in the task namespace. - minLength: 1 - type: string - required: - - name - type: object - type: - description: Type specifies which built-in CLI runtime to use. - Use runtimeRef for admin-registered custom runtimes. - enum: - - copilot - - claude - - codex - - opencode - type: string - type: object - x-kubernetes-validations: - - message: exactly one of type or runtimeRef is required - rule: has(self.type) != has(self.runtimeRef) - secretRef: - description: SecretRef references a Secret containing LLM API keys - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - session: - description: Session defines session configuration defaults - properties: - maxMessages: - default: 50 - description: MaxMessages is the maximum messages to load from - session - format: int32 - minimum: 1 - type: integer - persistence: - default: configmap - description: Persistence defines the storage backend (configmap, - pvc, none) - enum: - - configmap - - pvc - - none - type: string - ttl: - description: TTL defines the session time-to-live (auto-expire) - type: string - type: object - skills: - description: Skills lists the default skills for this agent - items: - description: SkillReference references a Skill CRD by name or inline - skill content from a ConfigMap key. - properties: - configMapRef: - description: ConfigMapRef references a ConfigMap key containing - skill text - properties: - key: - description: Key is the key within the ConfigMap - type: string - name: - description: Name is the name of the ConfigMap - type: string - required: - - key - - name - type: object - name: - description: Name references a Skill CR by name - type: string - type: object - type: array - systemPrompt: - description: SystemPrompt defines the system prompt configuration - properties: - configMapRef: - description: ConfigMapRef references a ConfigMap containing the - prompt - properties: - key: - description: Key is the key within the ConfigMap - type: string - name: - description: Name is the name of the ConfigMap - type: string - required: - - key - - name - type: object - inline: - description: Inline is the inline prompt text - type: string - type: object - tools: - description: Tools lists the default tools available to this agent - items: - description: ToolReference references a tool for an agent - properties: - enabled: - default: true - description: 'Enabled indicates if the tool is enabled (default: - true)' - type: boolean - name: - description: Name is the tool name (built-in or Tool CRD name) - type: string - required: - - name - type: object - type: array - ttlAfterLastTask: - description: |- - TTLAfterLastTask defines how long the agent persists after its last task completes. - When set and no tasks are active, the agent is deleted after this duration. - Zero means the agent is never auto-deleted (permanent). Default is no TTL (permanent). - type: string - type: object - status: - description: AgentStatus defines the observed state of Agent - properties: - activeTasks: - description: ActiveTasks is the number of active tasks using this - agent - format: int32 - type: integer - conditions: - description: Conditions represent the current state of the Agent - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - lastUsed: - description: LastUsed is the timestamp of when this agent was last - used - format: date-time - type: string - ready: - description: Ready indicates whether the agent configuration is valid - and usable - type: boolean - required: - - activeTasks - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: gatewaybindings.gateway.orka.ai -spec: - group: gateway.orka.ai - names: - kind: GatewayBinding - listKind: GatewayBindingList - plural: gatewaybindings - shortNames: - - gwbind - singular: gatewaybinding - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.gatewayRef.name - name: Gateway - type: string - - jsonPath: .spec.agentRef.name - name: Agent - type: string - - jsonPath: .spec.priority - name: Priority - type: integer - - jsonPath: .status.ready - name: Ready - type: boolean - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: GatewayBinding maps one normalized external context to one Agent. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: GatewayBindingSpec maps normalized external identity to one - Agent. - properties: - activeTurnBehavior: - description: ActiveTurnBehavior is queue in V1. - enum: - - queue - type: string - agentRef: - description: AgentRef selects one Agent in this namespace. - properties: - name: - description: Name is the object name. - minLength: 1 - type: string - required: - - name - type: object - gatewayRef: - description: GatewayRef selects one Gateway in this namespace. - properties: - name: - description: Name is the object name. - minLength: 1 - type: string - required: - - name - type: object - match: - description: Match selects exact normalized account/context identity - and optional constraints. - properties: - accountId: - description: AccountID is the exact normalized external account - identity. - maxLength: 256 - minLength: 1 - type: string - contextId: - description: ContextID is the exact normalized external conversation - or request context. - maxLength: 256 - minLength: 1 - type: string - senderId: - description: SenderID optionally restricts the binding to one - exact sender before sender policy is evaluated. - maxLength: 256 - type: string - threadId: - description: ThreadID optionally restricts the binding to one - exact thread. - maxLength: 256 - type: string - required: - - accountId - - contextId - type: object - priority: - description: Priority resolves overlapping matches. Equal-priority - matches fail closed. - format: int32 - maximum: 1000 - minimum: -1000 - type: integer - senderPolicy: - description: SenderPolicy denies unknown senders by default. - properties: - allowedSenderIds: - description: AllowedSenderIDs contains exact normalized sender - IDs. - items: - type: string - maxItems: 100 - type: array - x-kubernetes-list-type: set - mode: - description: Mode defaults to allowlist. all is an explicit trusted-context - opt-in. - enum: - - allowlist - - all - type: string - type: object - session: - description: Session controls deterministic Session derivation. - properties: - mode: - description: Mode selects the derivation inputs. - enum: - - ephemeral - - context - - thread - - sender - - context-sender - - thread-sender - - explicit - type: string - name: - description: Name is required only for explicit mode. - maxLength: 253 - type: string - type: object - taskDefaults: - description: TaskDefaults is the bounded Task configuration applied - to dispatched events. - properties: - agentRuntimeMaxTurns: - description: AgentRuntimeMaxTurns is the only runtime-specific - override permitted in V1. - format: int32 - maximum: 1000 - minimum: 1 - type: integer - priority: - description: Priority is the Task queue priority. - format: int32 - maximum: 1000 - minimum: 0 - type: integer - retryPolicy: - description: RetryPolicy configures bounded Task retries. - properties: - backoffMultiplier: - description: BackoffMultiplier controls exponential Task retry - delay. - maximum: 10 - minimum: 1 - type: number - initialDelay: - description: InitialDelay is the first Task retry delay. - type: string - x-kubernetes-validations: - - message: initialDelay must be positive and no greater than - 1h - rule: '!(duration(self) <= duration(''0s'') || duration(self) - > duration(''1h''))' - maxRetries: - description: MaxRetries is the maximum Task retry count. - format: int32 - maximum: 10 - minimum: 0 - type: integer - type: object - timeout: - description: Timeout is the Task timeout. - type: string - x-kubernetes-validations: - - message: timeout must be positive and no greater than 24h - rule: '!(duration(self) <= duration(''0s'') || duration(self) - > duration(''24h''))' - type: object - required: - - agentRef - - gatewayRef - - match - type: object - status: - description: GatewayBindingStatus reports routing readiness. - properties: - accepted: - description: Accepted reports spec validation. - type: boolean - conditions: - description: Conditions report Accepted, ResolvedRefs, Programmed, - and Ready state. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - lastInboundActivity: - description: LastInboundActivity is the latest accepted inbound event - time. - format: date-time - type: string - lastOutboundActivity: - description: LastOutboundActivity is the latest delivered outbound - reply time. - format: date-time - type: string - message: - description: Message is sanitized readiness context. - type: string - observedGeneration: - description: ObservedGeneration is the latest reconciled generation. - format: int64 - type: integer - programmed: - description: Programmed reports that the binding has no equal-priority - ambiguity. - type: boolean - ready: - description: Ready reports that ingress may route events through this - binding. - type: boolean - resolvedCapabilities: - description: ResolvedCapabilities is the provider-neutral capability - summary used for validation. - properties: - explicitSessions: - description: ExplicitSessions indicates that explicit Session - names are supported by bindings. - type: boolean - idempotentDelivery: - description: IdempotentDelivery indicates that repeated delivery - IDs do not create duplicate provider sends. - type: boolean - inboundText: - description: InboundText indicates support for authenticated inbound - text events. - type: boolean - outboundText: - description: OutboundText indicates support for final text and - sanitized error delivery. - type: boolean - senderIdentity: - description: SenderIdentity indicates that stable sender identity - is preserved. - type: boolean - threads: - description: Threads indicates that thread identity is preserved. - type: boolean - type: object - resolvedRefs: - description: ResolvedRefs reports Gateway and Agent resolution. - type: boolean - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: gatewayclasses.gateway.orka.ai -spec: - group: gateway.orka.ai - names: - kind: GatewayClass - listKind: GatewayClassList - plural: gatewayclasses - shortNames: - - gwclass - singular: gatewayclass - scope: Cluster - versions: - - additionalPrinterColumns: - - jsonPath: .status.accepted - name: Accepted - type: boolean - - jsonPath: .spec.contractVersion - name: Contract - type: string - - jsonPath: .spec.category - name: Category - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: GatewayClass is the cluster-scoped registry for gateway adapter - profiles. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: GatewayClassSpec defines one administrator-owned adapter - profile. - properties: - allowedMetadataKeys: - description: |- - AllowedMetadataKeys is the bounded allowlist of normalized event metadata keys. - Empty means no event metadata is accepted. - items: - type: string - maxItems: 32 - type: array - x-kubernetes-list-type: set - capabilities: - description: Capabilities are the behaviors every Gateway using this - class must advertise. - properties: - explicitSessions: - description: ExplicitSessions indicates that explicit Session - names are supported by bindings. - type: boolean - idempotentDelivery: - description: IdempotentDelivery indicates that repeated delivery - IDs do not create duplicate provider sends. - type: boolean - inboundText: - description: InboundText indicates support for authenticated inbound - text events. - type: boolean - outboundText: - description: OutboundText indicates support for final text and - sanitized error delivery. - type: boolean - senderIdentity: - description: SenderIdentity indicates that stable sender identity - is preserved. - type: boolean - threads: - description: Threads indicates that thread identity is preserved. - type: boolean - type: object - category: - description: Category describes the integration shape without selecting - a provider SDK. - enum: - - chat - - webhook - - http - - event - - internal - type: string - contractVersion: - description: ContractVersion is the adapter protocol implemented by - this class. - enum: - - orka.gateway.v1 - type: string - required: - - category - - contractVersion - type: object - status: - description: GatewayClassStatus reports class validation. - properties: - accepted: - description: Accepted reports whether the class is supported by Orka - core. - type: boolean - conditions: - description: Conditions report validation state. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - message: - description: Message is sanitized validation context. - type: string - observedGeneration: - description: ObservedGeneration is the latest reconciled generation. - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: gateways.gateway.orka.ai -spec: - group: gateway.orka.ai - names: - kind: Gateway - listKind: GatewayList - plural: gateways - shortNames: - - gw - singular: gateway - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.gatewayClassName - name: Class - type: string - - jsonPath: .status.ready - name: Ready - type: boolean - - jsonPath: .status.connected - name: Connected - type: boolean - - jsonPath: .status.resolvedEndpoint - name: Endpoint - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: Gateway configures one provider-neutral adapter instance. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: GatewaySpec defines one configured adapter instance. - properties: - adapter: - description: Adapter selects an HTTPS endpoint or TLS-authenticated - same-namespace Service. - properties: - endpoint: - description: Endpoint is an absolute HTTPS adapter base URL. - pattern: ^https://[^\s@?#]+$ - type: string - serviceRef: - description: ServiceRef selects a selector-backed TLS Service - in the Gateway namespace. - properties: - name: - description: Name is the Service name in the Gateway namespace. - minLength: 1 - type: string - port: - description: Port is the Service port. Zero selects the first - declared Service port. - format: int32 - maximum: 65535 - minimum: 0 - type: integer - required: - - name - type: object - type: object - x-kubernetes-validations: - - message: exactly one of endpoint or serviceRef is required - rule: has(self.endpoint) != has(self.serviceRef) - gatewayClassName: - description: GatewayClassName references a cluster-scoped GatewayClass. - minLength: 1 - type: string - inboundAuthRef: - description: InboundAuthRef authenticates adapter calls into Orka. - properties: - key: - description: Key is the Secret data key. - minLength: 1 - type: string - name: - description: Name is the Secret name. - minLength: 1 - type: string - x-kubernetes-validations: - - message: name must not contain surrounding whitespace - rule: self == self.trim() - required: - - key - - name - type: object - metadata: - additionalProperties: - type: string - description: Metadata is bounded, non-secret operator metadata. - maxProperties: 32 - type: object - outboundAuthRef: - description: OutboundAuthRef authenticates Orka calls to the adapter. - properties: - key: - description: Key is the Secret data key. - minLength: 1 - type: string - name: - description: Name is the Secret name. - minLength: 1 - type: string - x-kubernetes-validations: - - message: name must not contain surrounding whitespace - rule: self == self.trim() - required: - - key - - name - type: object - required: - - adapter - - gatewayClassName - - inboundAuthRef - - outboundAuthRef - type: object - x-kubernetes-validations: - - message: inbound and outbound authentication must use separate Secrets - rule: self.inboundAuthRef.name.trim() != self.outboundAuthRef.name.trim() - status: - description: GatewayStatus reports reference resolution and adapter connectivity. - properties: - accepted: - description: Accepted reports spec validation. - type: boolean - conditions: - description: Conditions report Accepted, ResolvedRefs, Connected, - and Ready state. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - connected: - description: Connected reports a successful authenticated adapter - probe. - type: boolean - lastSuccessfulProbe: - description: LastSuccessfulProbe is the last successful health/capability - probe. - format: date-time - type: string - message: - description: Message is sanitized readiness context. - type: string - observedCapabilities: - description: ObservedCapabilities is the sanitized adapter capability - response. - properties: - adapterName: - description: AdapterName is the sanitized adapter name. - type: string - adapterVersion: - description: AdapterVersion is the sanitized adapter version. - type: string - capabilities: - description: Capabilities are provider-neutral observed behaviors. - properties: - explicitSessions: - description: ExplicitSessions indicates that explicit Session - names are supported by bindings. - type: boolean - idempotentDelivery: - description: IdempotentDelivery indicates that repeated delivery - IDs do not create duplicate provider sends. - type: boolean - inboundText: - description: InboundText indicates support for authenticated - inbound text events. - type: boolean - outboundText: - description: OutboundText indicates support for final text - and sanitized error delivery. - type: boolean - senderIdentity: - description: SenderIdentity indicates that stable sender identity - is preserved. - type: boolean - threads: - description: Threads indicates that thread identity is preserved. - type: boolean - type: object - contractVersion: - description: ContractVersion is the protocol reported by the adapter. - type: string - type: object - observedGeneration: - description: ObservedGeneration is the latest reconciled generation. - format: int64 - type: integer - observedInboundAuthSecretVersion: - description: ObservedInboundAuthRefVersion is non-secret Secret resource - metadata. - type: string - observedOutboundAuthSecretVersion: - description: ObservedOutboundAuthRefVersion is non-secret Secret resource - metadata. - type: string - ready: - description: Ready reports that ingress and delivery may use this - Gateway. - type: boolean - resolvedEndpoint: - description: ResolvedEndpoint is a sanitized scheme and host suitable - for operators. - type: string - resolvedRefs: - description: ResolvedRefs reports class, Service, and Secret resolution. - type: boolean - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: providers.core.orka.ai -spec: - group: core.orka.ai - names: - kind: Provider - listKind: ProviderList - plural: providers - singular: provider - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.type - name: Type - type: string - - jsonPath: .status.ready - name: Ready - type: boolean - - jsonPath: .spec.defaultModel - name: Default Model - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: Provider is the Schema for the providers API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ProviderSpec defines the desired state of Provider - properties: - azure: - description: Azure contains Azure-specific configuration - properties: - apiVersion: - default: 2024-02-15-preview - description: APIVersion is the Azure OpenAI API version - type: string - deploymentName: - description: DeploymentName is the Azure OpenAI deployment name - type: string - required: - - deploymentName - type: object - baseURL: - description: BaseURL is an optional custom API endpoint (for proxies - or self-hosted) - type: string - defaultModel: - description: DefaultModel is the default model to use if not specified - in Task/Agent - type: string - rateLimit: - description: RateLimit configures rate limiting for this provider - properties: - requestsPerMinute: - description: RequestsPerMinute limits requests per minute - format: int32 - type: integer - tokensPerMinute: - description: TokensPerMinute limits tokens per minute - format: int64 - type: integer - type: object - secretRef: - description: SecretRef references the Secret containing API credentials - properties: - key: - default: api-key - description: Key is the key within the Secret containing the API - key - type: string - name: - description: Name is the name of the Secret - type: string - required: - - name - type: object - type: - description: Type is the LLM provider type (anthropic, openai, azure-openai) - enum: - - anthropic - - openai - - azure-openai - type: string - required: - - secretRef - - type - type: object - status: - description: ProviderStatus defines the observed state of Provider - properties: - conditions: - description: Conditions represent the current state of the Provider - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - lastValidated: - description: LastValidated is the timestamp of the last successful - validation - format: date-time - type: string - message: - description: Message provides additional status information - type: string - ready: - description: Ready indicates whether the provider is configured and - validated - type: boolean - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: repositorymonitors.core.orka.ai -spec: - group: core.orka.ai - names: - kind: RepositoryMonitor - listKind: RepositoryMonitorList - plural: repositorymonitors - singular: repositorymonitor - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.repository - name: Repo - type: string - - jsonPath: .spec.branch - name: Branch - type: string - - jsonPath: .spec.schedule - name: Schedule - type: string - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .status.pendingReviews - name: Pending - type: integer - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: RepositoryMonitor is the Schema for repository maintainer automation. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: RepositoryMonitorSpec defines the desired state of RepositoryMonitor. - properties: - agents: - description: Agents configures the agents used by monitor review and - repair tasks. - properties: - implementer: - description: Implementer is the agent used for guarded issue implementation - tasks. - properties: - name: - description: Name is the name of the Agent - type: string - namespace: - description: Namespace is the namespace of the Agent (defaults - to Task namespace) - type: string - required: - - name - type: object - repairer: - description: Repairer is the agent used for repair tasks. - properties: - name: - description: Name is the name of the Agent - type: string - namespace: - description: Namespace is the namespace of the Agent (defaults - to Task namespace) - type: string - required: - - name - type: object - reviewer: - description: Reviewer is the agent used for pull-request review - tasks. - properties: - name: - description: Name is the name of the Agent - type: string - namespace: - description: Namespace is the namespace of the Agent (defaults - to Task namespace) - type: string - required: - - name - type: object - type: object - automerge: - description: Automerge controls deterministic merge behavior. - properties: - allowedMergeMethods: - description: AllowedMergeMethods lists merge methods allowed by - policy. - items: - enum: - - merge - - squash - - rebase - type: string - type: array - x-kubernetes-list-type: set - enabled: - description: Enabled enables automerge jobs. - type: boolean - requireGlobalMergeGate: - default: true - description: RequireGlobalMergeGate requires the controller-wide - merge gate. - type: boolean - requireMaintainerOptIn: - default: true - description: RequireMaintainerOptIn requires explicit maintainer - opt-in. - type: boolean - type: object - branch: - default: main - description: Branch is the default base branch for repository-wide - monitoring decisions. - type: string - gitSecretRef: - description: GitSecretRef references GitHub credentials for repository - monitor operations. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - owner: - description: Owner is the repository owner or organization. - type: string - policy: - description: Policy contains authorization and safety policy for monitor - operations. - properties: - advisoryLabels: - description: AdvisoryLabels controls advisory label synchronization. - properties: - enabled: - description: Enabled enables advisory label sync. - type: boolean - type: object - allowedRepositoryPermissions: - description: AllowedRepositoryPermissions lists GitHub permissions - allowed to issue write commands. - items: - enum: - - admin - - maintain - - write - type: string - type: array - x-kubernetes-list-type: set - optInLabels: - description: OptInLabels names labels that opt a PR into repair - or automerge. - properties: - autofix: - description: Autofix opts a PR into autofix. - type: string - automerge: - description: Automerge opts a PR into automerge. - type: string - type: object - pauseLabels: - description: PauseLabels block further automation while present. - items: - type: string - type: array - x-kubernetes-list-type: set - protectedLabels: - description: ProtectedLabels block automated review, repair, or - merge according to policy. - items: - type: string - type: array - x-kubernetes-list-type: set - type: object - provider: - default: github - description: Provider is the source control provider. GitHub is the - only supported v1 provider. - enum: - - github - type: string - repair: - description: Repair controls bounded repair behavior. - properties: - enabled: - description: Enabled enables repair jobs. - type: boolean - maxRepairsPerHead: - description: MaxRepairsPerHead bounds automated repairs per PR - head SHA. - format: int32 - minimum: 0 - type: integer - maxRepairsPerPR: - description: MaxRepairsPerPR bounds total automated repairs per - PR. - format: int32 - minimum: 0 - type: integer - maxReviewFixRetries: - description: MaxReviewFixRetries bounds review-fix loop retries. - format: int32 - minimum: 0 - type: integer - maxValidationRetries: - description: MaxValidationRetries bounds validation retries for - one repair job. - format: int32 - minimum: 0 - type: integer - requireMaintainerOptIn: - default: true - description: RequireMaintainerOptIn requires an explicit maintainer - command or label. - type: boolean - type: object - repoURL: - description: RepoURL is the repository URL to monitor. - minLength: 1 - pattern: ^(https://github[.]com/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+([.]git)?/?|git@github[.]com:[A-Za-z0-9._-]+/[A-Za-z0-9._-]+([.]git)?)$ - type: string - repository: - description: Repository is the repository name. - type: string - review: - description: Review controls pull-request review behavior. - properties: - event: - default: COMMENT - description: |- - Event is the legacy/default GitHub review event value included in review task input. - It does not control RepositoryMonitor GitHub publishing; use Publish.Event. - enum: - - COMMENT - - APPROVE - - REQUEST_CHANGES - type: string - exactEventEnabled: - description: ExactEventEnabled enables exact-head review from - repository events. - type: boolean - publish: - description: |- - Publish controls deterministic GitHub pull request review publishing after review ingestion. - Publishing is disabled by default and V1 only supports neutral COMMENT reviews. - properties: - enabled: - description: Enabled enables GitHub pull request review publishing. - Defaults to false. - type: boolean - event: - default: COMMENT - description: Event is the GitHub review event to submit. V1 - only supports COMMENT. - enum: - - COMMENT - type: string - inline: - description: Inline controls optional inline review comments - for eligible findings. - properties: - enabled: - description: Enabled enables inline comments when Mode - is summary_with_inline_findings. - type: boolean - maxComments: - description: MaxComments caps inline comments per review. - Defaults to 10. - format: int32 - maximum: 50 - minimum: 0 - type: integer - minPriority: - default: P2 - description: MinPriority is the lowest finding priority - eligible for inline comments. Defaults to P2. - enum: - - P0 - - P1 - - P2 - - P3 - type: string - onlyChangedLines: - description: OnlyChangedLines restricts inline comments - to RIGHT-side changed lines. V1 treats this as true. - type: boolean - type: object - mode: - default: summary_only - description: Mode selects whether Orka publishes only a deterministic - summary or also eligible inline findings. - enum: - - summary_only - - summary_with_inline_findings - type: string - postNeedsChanges: - description: PostNeedsChanges controls whether needs_changes - reviews are posted. Defaults to true. - type: boolean - postNeedsHuman: - description: PostNeedsHuman controls whether needs_human reviews - are posted. Defaults to true. - type: boolean - postPassed: - description: PostPassed controls whether clean/passed reviews - are posted. Defaults to false. - type: boolean - postSecuritySensitive: - description: |- - PostSecuritySensitive allows public publishing of security_sensitive findings when true. - Defaults to false. - type: boolean - sameHeadPolicy: - default: skip - description: |- - SameHeadPolicy controls duplicate handling for one monitor, PR, and exact head SHA. - V1 only supports skip. - enum: - - skip - type: string - type: object - requireGreenCI: - description: RequireGreenCI requires acceptable CI before background - review selection. - type: boolean - staleReviewTTL: - description: StaleReviewTTL bounds how long an unchanged head - review remains fresh. - type: string - type: object - schedule: - description: Schedule is the cron expression for background monitor - runs. - type: string - suspend: - description: Suspend pauses scheduled monitor runs. - type: boolean - targets: - description: Targets selects the repository item types covered by - this monitor. - properties: - commits: - description: Commits configures commit monitoring. - properties: - enabled: - description: Enabled enables commit monitoring. - type: boolean - maxPerRun: - description: MaxPerRun limits commits selected by one background - run. - format: int32 - maximum: 100 - minimum: 1 - type: integer - type: object - issues: - description: Issues configures issue monitoring. - properties: - enabled: - description: Enabled enables issue monitoring. - type: boolean - maxPerRun: - description: MaxPerRun limits issues selected by one background - run. - format: int32 - maximum: 100 - minimum: 1 - type: integer - type: object - pullRequests: - description: PullRequests configures pull request monitoring. - properties: - enabled: - default: true - description: Enabled enables pull request monitoring. - type: boolean - includeDrafts: - description: IncludeDrafts allows draft pull requests to be - selected for review. - type: boolean - maxPerRun: - default: 20 - description: MaxPerRun limits pull requests selected by one - background run. - format: int32 - maximum: 100 - minimum: 1 - type: integer - type: object - type: object - timeZone: - description: TimeZone is the IANA time zone for the schedule. - type: string - validation: - description: Validation configures deterministic validation commands - for repair. - properties: - commands: - description: Commands are validation commands repair jobs must - run. - items: - type: string - type: array - mode: - default: changed - description: Mode selects validation scope. - enum: - - "off" - - changed - - full - type: string - type: object - required: - - repoURL - type: object - status: - description: RepositoryMonitorStatus defines the observed state of RepositoryMonitor. - properties: - activeRepairs: - description: ActiveRepairs is the count of repair jobs currently active. - format: int32 - type: integer - blockedItems: - description: BlockedItems is the count of items blocked by policy, - failures, or human action. - format: int32 - type: integer - conditions: - description: Conditions represent the current state of the repository - monitor. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - lastRunID: - description: LastRunID is the most recent monitor run identifier stored - in SQLite. - type: string - lastRunTime: - description: LastRunTime is the completion time of the most recent - run, regardless of success. - format: date-time - type: string - lastSuccessfulRunTime: - description: LastSuccessfulRunTime is the completion time of the most - recent successful run. - format: date-time - type: string - mergeReadyItems: - description: MergeReadyItems is the count of items ready for merge. - format: int32 - type: integer - observedGeneration: - description: ObservedGeneration is the latest spec generation reflected - in status. - format: int64 - type: integer - openPullRequests: - description: OpenPullRequests is the current count of open pull requests - seen by the monitor. - format: int32 - type: integer - pendingReviews: - description: PendingReviews is the count of items waiting for review. - format: int32 - type: integer - phase: - description: Phase describes the high-level monitor lifecycle state. - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: repositoryscans.core.orka.ai -spec: - group: core.orka.ai - names: - kind: RepositoryScan - listKind: RepositoryScanList - plural: repositoryscans - singular: repositoryscan - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.repository - name: Repo - type: string - - jsonPath: .spec.branch - name: Branch - type: string - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .status.findingCounts.total - name: Findings - type: integer - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: RepositoryScan is the Schema for the repository security scanning - API. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: RepositoryScanSpec defines the desired state of RepositoryScan. - properties: - analysisAgentRef: - description: AnalysisAgentRef is the agent used for scan runs. - properties: - name: - description: Name is the name of the Agent - type: string - namespace: - description: Namespace is the namespace of the Agent (defaults - to Task namespace) - type: string - required: - - name - type: object - branch: - description: Branch is the base branch to scan. Defaults to the literal - main branch when omitted. - type: string - customScanInstructionsRef: - description: CustomScanInstructionsRef references additive scanner - instructions in a same-namespace ConfigMap. - properties: - key: - description: Key is the ConfigMap data key. Defaults to "policy" - when omitted. - type: string - name: - description: Name is the ConfigMap name. - minLength: 1 - type: string - required: - - name - type: object - falsePositivePolicyRef: - description: FalsePositivePolicyRef references additive false-positive - policy in a same-namespace ConfigMap. - properties: - key: - description: Key is the ConfigMap data key. Defaults to "policy" - when omitted. - type: string - name: - description: Name is the ConfigMap name. - minLength: 1 - type: string - required: - - name - type: object - forkRepo: - description: ForkRepo is the writable fork repository URL used for - patch proposals. - type: string - gitSecretRef: - description: GitSecretRef references git credentials for private repositories. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - historyDays: - description: HistoryDays controls how far back the initial scan should - inspect repository history. - format: int32 - type: integer - maxFindingsPerRun: - description: MaxFindingsPerRun bounds scan output volume. - format: int32 - type: integer - owner: - description: Owner is the repository owner or organization. - type: string - patchAgentRef: - description: PatchAgentRef is the agent used for patch proposal runs. - properties: - name: - description: Name is the name of the Agent - type: string - namespace: - description: Namespace is the namespace of the Agent (defaults - to Task namespace) - type: string - required: - - name - type: object - prBaseBranch: - description: PRBaseBranch is the pull request base branch for remediation. - type: string - provider: - default: github - description: Provider is the source control provider. GitHub is the - only supported v1 provider. - enum: - - github - type: string - ref: - description: Ref is a specific git ref, tag, or commit SHA to checkout - for scan tasks. - type: string - repoURL: - description: RepoURL is the repository URL to scan. - type: string - repository: - description: Repository is the repository name. - type: string - schedule: - description: Schedule is the cron expression for incremental scans. - type: string - subPath: - description: SubPath scopes scanning to a subdirectory in a monorepo. - type: string - suspend: - description: Suspend pauses scheduled incremental scans. - type: boolean - timeZone: - description: TimeZone is the IANA time zone for the schedule. - type: string - validationMaxFindingsPerRun: - description: ValidationMaxFindingsPerRun bounds automatic validation - tasks for light mode. - format: int32 - minimum: 0 - type: integer - validationMinConfidence: - description: ValidationMinConfidence is the minimum confidence eligible - for automatic validation. - enum: - - high - - medium - - low - type: string - validationMinSeverity: - description: ValidationMinSeverity is the minimum severity eligible - for automatic validation. - enum: - - critical - - high - - medium - - low - type: string - validationMode: - default: light - description: ValidationMode controls how aggressively findings are - validated. - enum: - - "off" - - light - - full - type: string - required: - - analysisAgentRef - - repoURL - type: object - status: - description: RepositoryScanStatus defines the observed state of RepositoryScan. - properties: - conditions: - description: Conditions represent the current state of the repository - scan. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - findingCounts: - description: FindingCounts summarizes open findings. - properties: - critical: - format: int32 - type: integer - high: - format: int32 - type: integer - low: - format: int32 - type: integer - medium: - format: int32 - type: integer - total: - format: int32 - type: integer - type: object - lastObservedHeadSHA: - description: LastObservedHeadSHA is the latest repository head SHA - seen by a completed scan. - type: string - lastProcessedCommit: - description: LastProcessedCommit is the latest commit fully processed - by a completed scan. - type: string - lastScanAt: - description: LastScanAt is the completion time of the most recent - scan run, regardless of success or failure. - format: date-time - type: string - lastScanID: - description: LastScanID is the most recent scan run identifier stored - in SQLite. - type: string - lastScanTaskName: - description: LastScanTaskName is the most recent scan task name. - type: string - lastSuccessfulScanAt: - description: LastSuccessfulScanAt is the completion time of the most - recent successful scan. - format: date-time - type: string - phase: - description: Phase describes the high-level repository scan lifecycle - state. - type: string - threatModelVersion: - description: ThreatModelVersion is the latest persisted threat model - version. - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: skills.core.orka.ai -spec: - group: core.orka.ai - names: - kind: Skill - listKind: SkillList - plural: skills - singular: skill - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.displayName - name: Display Name - type: string - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .spec.version - name: Version - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: Skill is the Schema for the skills API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: SkillSpec defines the desired state of Skill - properties: - author: - description: Author is the author or maintainer of the skill - type: string - content: - description: Content defines the skill content (Agent Skills standard) - properties: - files: - additionalProperties: - type: string - description: |- - Files is a map of additional files (templates, examples) mounted alongside the skill - Keys are relative paths (e.g. "templates/review-checklist.md") - type: object - inline: - description: Inline is the SKILL.md content injected into the - system prompt - type: string - required: - - inline - type: object - description: - description: Description is a short description of the skill shown - to users and LLMs - type: string - displayName: - description: DisplayName is the human-readable name of the skill - type: string - source: - description: Source tracks where this skill was imported from (for - updates) - properties: - context7: - description: Context7 indicates whether this skill is indexed - by Context7 - type: boolean - github: - description: GitHub is the GitHub repo path (e.g. "/anthropics/skills") - type: string - skillName: - description: SkillName is the skill name within the source repo - type: string - type: object - tags: - description: Tags are labels for categorization and discovery - items: - type: string - type: array - version: - description: Version is the semantic version of the skill - type: string - required: - - content - - description - type: object - status: - description: SkillStatus defines the observed state of Skill - properties: - conditions: - description: Conditions represent the current state of the Skill - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - contentHash: - description: ContentHash is the SHA-256 hash of the skill content - for change detection - type: string - observedGeneration: - description: ObservedGeneration is the most recent generation observed - format: int64 - type: integer - phase: - description: 'Phase indicates the current state of the skill: Ready - or Error' - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: substrateactorpools.core.orka.ai -spec: - group: core.orka.ai - names: - kind: SubstrateActorPool - listKind: SubstrateActorPoolList - plural: substrateactorpools - singular: substrateactorpool - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .status.actorCount - name: Actors - type: integer - - jsonPath: .status.workerCount - name: Workers - type: integer - - jsonPath: .status.actorsPerWorker - name: Density - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: SubstrateActorPool is the Schema for Substrate actor pools. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: SubstrateActorPoolSpec defines an operator-owned oversubscription - pool. - properties: - precreateActors: - description: |- - PrecreateActors asks the controller to create deterministic warm actors up - to TargetActors. Substrate may suspend them when the WorkerPool is full. - type: boolean - targetActors: - description: |- - TargetActors is the desired number of stateful actors tracked for this - pool. It may exceed TargetWorkers to express oversubscription. - format: int32 - maximum: 1000 - minimum: 0 - type: integer - targetWorkers: - description: TargetWorkers is the intended physical worker budget - for this pool. - format: int32 - minimum: 0 - type: integer - templateRef: - description: TemplateRef is the ActorTemplate used for pool members. - properties: - name: - description: Name is the name of the workspace template. - type: string - namespace: - description: |- - Namespace is the namespace of the workspace template and claim. - It defaults to the Task namespace, or the controller namespace when configured. - type: string - type: object - workerPoolRef: - description: WorkerPoolRef is the Substrate WorkerPool this Orka pool - targets. - properties: - name: - description: Name is the name of the workspace template. - type: string - namespace: - description: |- - Namespace is the namespace of the workspace template and claim. - It defaults to the Task namespace, or the controller namespace when configured. - type: string - type: object - required: - - templateRef - type: object - status: - description: SubstrateActorPoolStatus reports safe pool telemetry. - properties: - actorCount: - description: ActorCount is the number of actors reported by Substrate - for this pool. - format: int32 - type: integer - actorsPerWorker: - description: ActorsPerWorker is ActorCount divided by WorkerCount, - formatted as a decimal string. - type: string - conditions: - description: Conditions represent the current state of the pool. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - message: - description: Message contains sanitized reconciliation context. - type: string - observedGeneration: - description: ObservedGeneration is the latest generation reconciled - by the controller. - format: int64 - type: integer - phase: - description: Phase is the current controller-observed phase. - enum: - - Pending - - Ready - - Failed - type: string - runningActorCount: - description: RunningActorCount is the number of pool actors currently - running. - format: int32 - type: integer - suspendedActorCount: - description: SuspendedActorCount is the number of pool actors currently - suspended. - format: int32 - type: integer - workerCount: - description: WorkerCount is the number of workers reported by Substrate - for this pool. - format: int32 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition +apiVersion: v1 +kind: ServiceAccount metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - gateway.orka.ai/session-cutoff-schema: v1 - name: tasks.core.orka.ai -spec: - group: core.orka.ai - names: - kind: Task - listKind: TaskList - plural: tasks - singular: task - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.type - name: Type - type: string - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .spec.priority - name: Priority - type: integer - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: Task is the Schema for the tasks API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: TaskSpec defines the desired state of Task - properties: - agentRef: - description: AgentRef references an Agent CRD for configuration - properties: - name: - description: Name is the name of the Agent - type: string - namespace: - description: Namespace is the namespace of the Agent (defaults - to Task namespace) - type: string - required: - - name - type: object - agentRuntime: - description: AgentRuntime contains task-level overrides for agent - runtime configuration (when type is "agent") - properties: - allowBash: - description: AllowBash enables the agent to run bash commands - (overrides Agent default) - type: boolean - allowedTools: - description: AllowedTools lists the tools the agent is allowed - to use (overrides Agent defaults) - items: - type: string - type: array - disallowedTools: - description: DisallowedTools lists tools the agent is not allowed - to use - items: - type: string - type: array - maxTurns: - description: MaxTurns limits the number of agent loop iterations - format: int32 - maximum: 1000 - minimum: 1 - type: integer - workspace: - description: Workspace defines the working directory configuration - properties: - branch: - description: Branch is the git branch to checkout - type: string - forkRepo: - description: ForkRepo is the writable fork repository URL - for pushing changes - type: string - gitRepo: - description: GitRepo is the repository URL to clone - type: string - gitSecretRef: - description: GitSecretRef references a Secret containing git - credentials - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - prBaseBranch: - description: PRBaseBranch is the upstream branch to target - for pull requests - type: string - pushBranch: - description: |- - PushBranch is the remote branch name to push changes to after the agent completes. - When set, FinalizeResult will commit and push changes to this branch. - type: string - ref: - description: Ref is a specific git ref (commit SHA, tag) to - checkout - type: string - subPath: - description: SubPath is a subdirectory within the repo to - use as workspace root - type: string - type: object - type: object - ai: - description: AI contains AI-specific configuration (when type is "ai") - properties: - maxTokens: - description: MaxTokens limits the response length - format: int32 - type: integer - model: - description: Model is the model identifier - required if providerRef - not set - type: string - prompt: - description: Prompt is the user prompt for the AI task - type: string - provider: - description: Provider is the LLM provider (anthropic, openai) - - required if providerRef not set - enum: - - anthropic - - openai - type: string - providerRef: - description: |- - ProviderRef references a Provider CRD for LLM configuration - If set, provider and model fields are optional (defaults from Provider) - properties: - name: - description: Name is the name of the Provider - type: string - namespace: - description: Namespace is the namespace of the Provider (defaults - to Task namespace) - type: string - required: - - name - type: object - skills: - description: Skills references Skill CRDs to inject into the agent's - system prompt - items: - description: SkillReference references a Skill CRD by name or - inline skill content from a ConfigMap key. - properties: - configMapRef: - description: ConfigMapRef references a ConfigMap key containing - skill text - properties: - key: - description: Key is the key within the ConfigMap - type: string - name: - description: Name is the name of the ConfigMap - type: string - required: - - key - - name - type: object - name: - description: Name references a Skill CR by name - type: string - type: object - type: array - systemPrompt: - description: SystemPrompt is an optional system prompt - type: string - temperature: - description: Temperature controls randomness in generation - maximum: 2 - minimum: 0 - type: number - tools: - description: Tools lists the tools available for this task - items: - type: string - type: array - type: object - args: - description: Args are the arguments to pass to the command - items: - type: string - type: array - command: - description: Command is the command to run in the container - items: - type: string - type: array - concurrencyPolicy: - default: Forbid - description: ConcurrencyPolicy specifies how to treat concurrent runs - (Allow or Forbid). - enum: - - Allow - - Forbid - type: string - env: - description: Env is a list of environment variables to set in the - container - items: - description: EnvVar represents an environment variable present in - a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. Cannot - be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is - written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified - API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing - the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed - resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must - be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - execution: - description: Execution defines worker pod runtime and placement settings. - properties: - affinity: - description: Affinity defines Kubernetes affinity and anti-affinity - rules for worker pods. - properties: - nodeAffinity: - description: Describes node affinity scheduling rules for - the pod. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, etc.), - compute a sum by iterating through the elements of this field and adding - "weight" to the sum if the node matches the corresponding matchExpressions; the - node(s) with the highest sum are the most preferred. - items: - description: |- - An empty preferred scheduling term matches all objects with implicit weight 0 - (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). - properties: - preference: - description: A node selector term, associated with - the corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - type: object - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the - corresponding nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from its node. - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: |- - A null or empty node selector term matches no objects. The requirements of - them are ANDed. - The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: |- - A node selector requirement is a selector that contains values, a key, and an operator - that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: |- - Represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: |- - An array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. If the operator is Gt or Lt, the values - array must have a single element, which will be interpreted as an integer. - This array is replaced during a strategic merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - type: object - x-kubernetes-map-type: atomic - type: array - x-kubernetes-list-type: atomic - required: - - nodeSelectorTerms - type: object - x-kubernetes-map-type: atomic - type: object - podAffinity: - description: Describes pod affinity scheduling rules (e.g. - co-locate this pod in the same node, zone, etc. as some - other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling affinity expressions, etc.), - compute a sum by iterating through the elements of this field and adding - "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: |- - weight associated with matching the corresponding podAffinityTerm, - in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod label update), the - system may or may not try to eventually evict the pod from its node. - When there are multiple elements, the lists of nodes corresponding to each - podAffinityTerm are intersected, i.e. all terms must be satisfied. - items: - description: |- - Defines a set of pods (namely those matching the labelSelector - relative to the given namespace(s)) that this pod should be - co-located (affinity) or not co-located (anti-affinity) with, - where co-located is defined as running on a node whose value of - the label with key matches that of any node on which - a pod of the set of pods is running - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - x-kubernetes-list-type: atomic - type: object - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules - (e.g. avoid putting this pod in the same node, zone, etc. - as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: |- - The scheduler will prefer to schedule pods to nodes that satisfy - the anti-affinity expressions specified by this field, but it may choose - a node that violates one or more of the expressions. The node that is - most preferred is the one with the greatest sum of weights, i.e. - for each node that meets all of the scheduling requirements (resource - request, requiredDuringScheduling anti-affinity expressions, etc.), - compute a sum by iterating through the elements of this field and subtracting - "weight" from the sum if the node has pods which matches the corresponding podAffinityTerm; the - node(s) with the highest sum are the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: |- - weight associated with matching the corresponding podAffinityTerm, - in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - x-kubernetes-list-type: atomic - requiredDuringSchedulingIgnoredDuringExecution: - description: |- - If the anti-affinity requirements specified by this field are not met at - scheduling time, the pod will not be scheduled onto the node. - If the anti-affinity requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod label update), the - system may or may not try to eventually evict the pod from its node. - When there are multiple elements, the lists of nodes corresponding to each - podAffinityTerm are intersected, i.e. all terms must be satisfied. - items: - description: |- - Defines a set of pods (namely those matching the labelSelector - relative to the given namespace(s)) that this pod should be - co-located (affinity) or not co-located (anti-affinity) with, - where co-located is defined as running on a node whose value of - the label with key matches that of any node on which - a pod of the set of pods is running - properties: - labelSelector: - description: |- - A label query over a set of resources, in this case pods. - If it's null, this PodAffinityTerm matches with no Pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - matchLabelKeys: - description: |- - MatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both matchLabelKeys and labelSelector. - Also, matchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - mismatchLabelKeys: - description: |- - MismatchLabelKeys is a set of pod label keys to select which pods will - be taken into consideration. The keys are used to lookup values from the - incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` - to select the group of existing pods which pods will be taken into consideration - for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming - pod labels will be ignored. The default value is empty. - The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. - Also, mismatchLabelKeys cannot be set when labelSelector isn't set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - namespaceSelector: - description: |- - A label query over the set of namespaces that the term applies to. - The term is applied to the union of the namespaces selected by this field - and the ones listed in the namespaces field. - null selector and null or empty namespaces list means "this pod's namespace". - An empty selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: |- - namespaces specifies a static list of namespace names that the term applies to. - The term is applied to the union of the namespaces listed in this field - and the ones selected by namespaceSelector. - null or empty namespaces list and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - x-kubernetes-list-type: atomic - topologyKey: - description: |- - This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching - the labelSelector in the specified namespaces, where co-located is defined as running on a node - whose value of the label with key topologyKey matches that of any node on which any of the - selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - x-kubernetes-list-type: atomic - type: object - type: object - nodeSelector: - additionalProperties: - type: string - description: NodeSelector constrains worker pods to nodes with - matching labels. - type: object - runtimeClassName: - description: RuntimeClassName routes worker pods through a specific - RuntimeClass. - type: string - tolerations: - description: Tolerations allows worker pods to schedule onto tainted - nodes. - items: - description: |- - The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . - properties: - effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. - type: string - operator: - description: |- - Operator represents a key's relationship to the value. - Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. - Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). - type: string - tolerationSeconds: - description: |- - TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). Zero and - negative values will be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. - type: string - type: object - type: array - workspace: - description: |- - Workspace requests an upstream agent-sandbox execution workspace for agent Tasks. - When enabled, the Task controller validates the request and propagates the - resolved sandbox settings to the agent worker Job. The worker wrapper then - claims the sandbox workspace and runs the configured agent runtime inside it. - properties: - boot: - description: |- - Boot asks providers that support it to boot the workspace workload from scratch - instead of resuming from the provider's default snapshot. Currently supported - by the Substrate provider. - type: boolean - cleanupPolicy: - description: |- - CleanupPolicy controls whether the workspace is deleted or retained after use. - Defaults to delete when omitted. - enum: - - delete - - retain - type: string - enabled: - default: false - description: Enabled requests use of a durable workspace for - the task execution. - type: boolean - hibernation: - description: |- - Hibernation configures process lifetime inside the workspace. Resident - mode is currently rejected until the worker protocol can report per-turn - completion separately from resident process lifetime. - properties: - processMode: - description: |- - ProcessMode controls whether each turn starts fresh or reuses a resident - process. Defaults to fresh. - enum: - - fresh - - resident - type: string - residentKey: - description: |- - ResidentKey selects the process slot for resident mode. When omitted, the - worker derives a stable key from namespace, template, and reuse key. - type: string - type: object - poolRef: - description: |- - PoolRef references an operator-managed Substrate actor pool for placement, - density tracking, and oversubscription policy. - properties: - name: - description: Name is the pool name. - type: string - namespace: - description: Namespace is the pool namespace. It defaults - to the Task namespace. - type: string - type: object - provider: - description: |- - Provider selects the workspace backend. When omitted, the controller - resolves the configured default workspace provider; the built-in - compatibility default is agent-sandbox. - enum: - - agent-sandbox - - substrate - type: string - reusePolicy: - default: none - description: |- - ReusePolicy controls whether the workspace is fresh or session-scoped. - Defaults to none when omitted. - enum: - - none - - session - type: string - snapshot: - description: |- - Snapshot configures explicit provider snapshot restore/checkpoint behavior. - Non-empty settings are currently rejected until provider checkpoint/restore - support is available through Orka. - properties: - checkpointOnRelease: - description: |- - CheckpointOnRelease requests a checkpoint when cleanup releases or retains - the workspace. CheckpointURI must be set when this is true. - type: boolean - checkpointURI: - description: |- - CheckpointURI is a provider-native snapshot URI prefix to write when the - workspace is retained or released. - type: string - restoreURI: - description: |- - RestoreURI is a provider-native snapshot URI prefix to restore before the - workspace command runs. - type: string - type: object - templateRef: - description: |- - TemplateRef references the workspace template to instantiate or reuse. - The template name is required when enabled is true unless the controller - is configured with a provider-specific default template. - properties: - name: - description: Name is the name of the workspace template. - type: string - namespace: - description: |- - Namespace is the namespace of the workspace template and claim. - It defaults to the Task namespace, or the controller namespace when configured. - type: string - type: object - type: object - type: object - failedRunsHistoryLimit: - default: 1 - description: FailedRunsHistoryLimit is the number of failed child - tasks to retain. - format: int32 - minimum: 0 - type: integer - image: - description: Image is the container image to run for the task - type: string - priorTaskRef: - description: |- - PriorTaskRef references a previously completed task whose diff should be - applied to the workspace before this task begins execution. - properties: - name: - description: Name is the name of the prior task - type: string - namespace: - description: Namespace is the namespace of the prior task (defaults - to Task namespace) - type: string - required: - - name - type: object - priority: - default: 500 - description: Priority is the queue priority (0-1000, higher = more - urgent) - format: int32 - maximum: 1000 - minimum: 0 - type: integer - prompt: - description: Prompt is the task-specific prompt (used with agentRef) - type: string - requestedBy: - description: |- - RequestedBy records the verified identity that created the task. - This field is populated by the API server and is immutable. - properties: - email: - description: Email is the email claim, if present. - type: string - groups: - description: Groups are verified group values, if present. - items: - type: string - type: array - issuer: - description: Issuer is the token issuer that authenticated the - requester. - type: string - roles: - description: Roles are verified role or scope values, if present. - items: - type: string - type: array - subject: - description: Subject is the verified subject claim. - type: string - username: - description: Username is the verified username, if present. - type: string - type: object - resources: - description: Resources defines the compute resources for the task - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - retryPolicy: - description: RetryPolicy defines the retry behavior for failed tasks - properties: - backoffMultiplier: - default: 2 - description: BackoffMultiplier is the exponential backoff multiplier - type: number - initialDelay: - description: InitialDelay is the initial delay before the first - retry - type: string - maxRetries: - default: 0 - description: MaxRetries is the maximum number of retry attempts - format: int32 - minimum: 0 - type: integer - type: object - schedule: - description: |- - Schedule is a cron expression for recurring tasks (e.g., "0 */6 * * *"). - When set, the controller creates child Task CRs on each cron tick. - type: string - secretRef: - description: SecretRef references a Kubernetes Secret containing credentials - properties: - name: - description: Name is the name of the Secret - type: string - namespace: - description: Namespace is the namespace of the Secret (defaults - to Task namespace) - type: string - required: - - name - type: object - sessionRef: - description: SessionRef references a session for conversation continuity - properties: - append: - default: true - description: Append indicates whether to append task messages - to the session transcript - type: boolean - create: - default: false - description: Create indicates whether to create the session if - it doesn't exist - type: boolean - maxMessages: - default: 50 - description: MaxMessages is the maximum number of messages to - load from session - format: int32 - minimum: 1 - type: integer - name: - description: 'Name is the session identifier (ConfigMap: session-)' - type: string - promptIncluded: - description: |- - PromptIncluded reports that the current Task prompt is already the final user message in the bounded transcript. - Workers must not append prompt a second time when this is true. - type: boolean - throughMessageId: - description: |- - ThroughMessageID limits transcript loading to the logical history at and before this stable message ID. - Gateway-created Tasks use it so later queued user messages cannot enter an earlier turn. - maxLength: 256 - type: string - required: - - name - type: object - startingDeadlineSeconds: - default: 100 - description: |- - StartingDeadlineSeconds is the deadline in seconds for starting a missed scheduled run. - If the schedule is missed by more than this many seconds, the run is skipped. - format: int64 - type: integer - successfulRunsHistoryLimit: - default: 3 - description: SuccessfulRunsHistoryLimit is the number of successful - child tasks to retain. - format: int32 - minimum: 0 - type: integer - suspend: - description: |- - Suspend tells the controller to suspend subsequent scheduled runs. - It does not apply to already started child tasks. Defaults to false. - type: boolean - timeZone: - description: |- - TimeZone is the IANA time zone for the schedule (e.g., "America/New_York"). - Defaults to UTC if not set. - type: string - timeout: - description: Timeout is the maximum duration for the task - type: string - transaction: - description: |- - Transaction records verified transaction-token metadata for audit correlation. - This field is populated by the API server and is immutable. - properties: - audience: - description: Audience lists the verified token audience values. - items: - type: string - type: array - context: - additionalProperties: - type: string - description: Context contains allowlisted, non-sensitive transaction - context fields for audit. - type: object - contextDigest: - description: ContextDigest is a SHA256 digest of the full transaction - context. - type: string - id: - description: ID is the verified transaction identifier claim. - type: string - issuer: - description: Issuer is the token issuer that authenticated the - transaction. - type: string - profile: - description: Profile is the context-token profile that authenticated - the request. - type: string - requesterContextDigest: - description: RequesterContextDigest is a SHA256 digest of the - full requester context. - type: string - requestingWorkload: - description: RequestingWorkload is the verified workload that - requested the transaction. - type: string - scope: - description: Scope is the original verified scope string. - type: string - scopes: - description: Scopes lists parsed scope values from the verified - scope string. - items: - type: string - type: array - subject: - description: Subject is the verified subject claim. - type: string - type: object - type: - description: 'Type specifies the task type: "container" or "ai"' - enum: - - container - - ai - - agent - type: string - webhookURL: - description: WebhookURL is the URL to call when the task completes - type: string - workspace: - description: |- - Workspace defines repository checkout and push settings for tasks that need - a git workspace. Agent tasks can continue to use agentRuntime.workspace for - compatibility; this top-level field is used by container tasks as well. - properties: - branch: - description: Branch is the git branch to checkout - type: string - forkRepo: - description: ForkRepo is the writable fork repository URL for - pushing changes - type: string - gitRepo: - description: GitRepo is the repository URL to clone - type: string - gitSecretRef: - description: GitSecretRef references a Secret containing git credentials - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - prBaseBranch: - description: PRBaseBranch is the upstream branch to target for - pull requests - type: string - pushBranch: - description: |- - PushBranch is the remote branch name to push changes to after the agent completes. - When set, FinalizeResult will commit and push changes to this branch. - type: string - ref: - description: Ref is a specific git ref (commit SHA, tag) to checkout - type: string - subPath: - description: SubPath is a subdirectory within the repo to use - as workspace root - type: string - type: object - required: - - type - type: object - x-kubernetes-validations: - - message: requestedBy is immutable - rule: has(self.requestedBy) == has(oldSelf.requestedBy) && (!has(self.requestedBy) - || self.requestedBy == oldSelf.requestedBy) - - message: transaction is immutable - rule: has(self.transaction) == has(oldSelf.transaction) && (!has(self.transaction) - || self.transaction == oldSelf.transaction) - status: - description: TaskStatus defines the observed state of Task - properties: - attempts: - description: Attempts is the number of attempts made - format: int32 - type: integer - childTasks: - description: ChildTasks tracks delegated child tasks (for coordinator - agents) - items: - description: ChildTaskStatus tracks the status of a delegated child - task - properties: - agent: - description: Agent is the agent handling the child task - type: string - name: - description: Name is the name of the child task - type: string - phase: - description: Phase is the current phase of the child task - enum: - - Pending - - Running - - Succeeded - - Failed - - Scheduled - - Cancelled - type: string - result: - description: Result is the result from the child task (if completed) - type: string - required: - - agent - - name - - phase - type: object - type: array - completionTime: - description: CompletionTime is when the task completed - format: date-time - type: string - conditions: - description: Conditions represent the current state of the Task - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - executionWorkspace: - description: |- - ExecutionWorkspace reports the provider-neutral lifecycle state for a - requested execution workspace. Provider-native identifiers and credentials - are intentionally omitted. - properties: - cleanupPolicy: - description: CleanupPolicy is the resolved cleanup policy. - enum: - - delete - - retain - type: string - density: - description: Density reports non-secret actor and worker counts - for the workspace provider. - properties: - actorCount: - description: ActorCount is the number of actors reported by - the provider. - format: int32 - type: integer - actorsPerWorker: - description: ActorsPerWorker is ActorCount divided by WorkerCount, - formatted as a decimal string. - type: string - runningActorCount: - description: RunningActorCount is the number of actors currently - running on workers. - format: int32 - type: integer - suspendedActorCount: - description: SuspendedActorCount is the number of actors currently - suspended. - format: int32 - type: integer - workerCount: - description: WorkerCount is the number of workers reported - by the provider. - format: int32 - type: integer - type: object - lastUpdateTime: - description: LastUpdateTime is the last time workspace status - was updated. - format: date-time - type: string - message: - description: Message contains sanitized lifecycle context. - type: string - phase: - description: Phase is the provider-neutral lifecycle phase. - enum: - - Pending - - Ready - - Released - - Retained - - Deleted - - Failed - type: string - placement: - description: Placement reports non-secret runtime placement metadata - for the workspace. - properties: - workerNamespace: - description: WorkerNamespace is the namespace containing the - selected worker pod. - type: string - workerPodName: - description: WorkerPodName is the selected worker pod name - when available. - type: string - workerPool: - description: WorkerPool is the provider's worker-pool name - when available. - type: string - type: object - provider: - description: Provider is the resolved workspace backend. - enum: - - agent-sandbox - - substrate - type: string - reason: - description: Reason is the provider-neutral lifecycle reason. - enum: - - WorkspacePending - - WorkspaceClaimed - - WorkspaceReady - - WorkspaceReleased - - WorkspaceRetained - - WorkspaceDeleted - - WorkspaceValidationFailed - - WorkspaceAttachmentLocked - - WorkspaceClaimFailed - - WorkspaceReadinessFailed - - WorkspaceHandoffFailed - - WorkspaceCommandFailed - - WorkspaceSecretScrubFailed - - WorkspaceCleanupFailed - - WorkspaceStatusUpdateFailed - type: string - resumeLatency: - description: ResumeLatency is the observed time spent resuming - the workspace until it was ready. - type: string - reusePolicy: - description: ReusePolicy is the resolved reuse policy. - enum: - - none - - session - type: string - reused: - description: Reused reports whether an existing workspace was - reattached. - type: boolean - templateRef: - description: TemplateRef is the resolved workspace template. - properties: - name: - description: Name is the name of the workspace template. - type: string - namespace: - description: |- - Namespace is the namespace of the workspace template and claim. - It defaults to the Task namespace, or the controller namespace when configured. - type: string - type: object - type: object - harnessRuntime: - description: |- - HarnessRuntime records the controller-resolved harness runtime target for an - in-flight agent turn. It intentionally stores only non-secret routing metadata - and Secret references, never bearer values. - properties: - authRefField: - description: AuthRefField is the Secret data field selected when - the turn started. - type: string - authRefName: - description: AuthRefName is the Secret name selected when the - turn started. - type: string - authRefResourceVersion: - description: AuthRefResourceVersion is the auth Secret resourceVersion - validated before starting the turn. - type: string - contractVersion: - description: ContractVersion is the Orka harness contract version - used for the turn. - type: string - endpoint: - description: Endpoint is the non-secret harness base URL selected - when the turn started. - type: string - runtimeGeneration: - description: RuntimeGeneration is the AgentRuntime generation - selected when the turn started. - format: int64 - type: integer - runtimeName: - description: RuntimeName is the runtime name advertised by the - harness capabilities and sent in turn metadata. - type: string - runtimeRefName: - description: RuntimeRefName is the AgentRuntime name for custom - runtimeRef turns. Empty means built-in CLI wrapper. - type: string - type: object - iteration: - description: |- - Iteration is the current autonomous loop iteration (0-based). - Only used when the task's coordination config has autonomous mode enabled. - format: int32 - type: integer - jobName: - description: JobName is the name of the Kubernetes Job running the - task - type: string - lastScheduleTime: - description: LastScheduleTime is the last time a child task was created - for a scheduled run. - format: date-time - type: string - message: - description: Message provides additional status information - type: string - nextScheduleTime: - description: NextScheduleTime is the next time a child task will be - created. - format: date-time - type: string - phase: - description: Phase is the current phase of the task - enum: - - Pending - - Running - - Succeeded - - Failed - - Scheduled - - Cancelled - type: string - resultRef: - description: ResultRef indicates whether a result is available - properties: - available: - description: Available indicates whether a result has been stored - for this task - type: boolean - required: - - available - type: object - startTime: - description: StartTime is when the task started running - format: date-time - type: string - webhookDelivered: - description: WebhookDelivered indicates whether the webhook was successfully - called - type: boolean - type: object - type: object - served: true - storage: true - subresources: - status: {} + name: orka-ai-worker + namespace: orka-system --- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition +apiVersion: v1 +kind: ServiceAccount metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: tools.core.orka.ai -spec: - group: core.orka.ai - names: - kind: Tool - listKind: ToolList - plural: tools - singular: tool - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.http.url - name: URL - type: string - - jsonPath: .status.available - name: Available - type: boolean - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: Tool is the Schema for the tools API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ToolSpec defines the desired state of Tool - properties: - brokeredToolClass: - description: |- - BrokeredToolClass declares the governed class for remote AgentRuntime brokered calls. - Tools without this field are not exposed to remote runtimes through brokered governance. - enum: - - read - - write - - coordination - type: string - description: - description: Description is the tool description shown to the LLM - type: string - http: - description: |- - HTTP defines the HTTP execution configuration. It is required unless MCP - is set. - properties: - authBodyKey: - description: |- - AuthBodyKey is the JSON key name to use when AuthInject=body - For example, "api_key" would inject {"api_key": "", ...} - Only used when AuthInject=body - type: string - authInject: - default: header - description: |- - AuthInject specifies where to inject the auth token: "header" (default) or "body" - When "header", the token is added as Authorization: Bearer - When "body", the token is added to the JSON request body using AuthBodyKey - enum: - - header - - body - type: string - authSecretRef: - description: |- - AuthSecretRef references a Secret containing authentication token - Token is injected as Authorization: Bearer (when AuthInject=header) - or into the request body (when AuthInject=body) - properties: - key: - description: Key is the key within the Secret - type: string - name: - description: Name is the name of the Secret - type: string - required: - - key - - name - type: object - headers: - additionalProperties: - type: string - description: Headers are additional headers to include in requests - type: object - method: - default: POST - description: 'Method is the HTTP method to use (default: POST)' - enum: - - GET - - POST - - PUT - - PATCH - - DELETE - type: string - timeout: - description: 'Timeout is the request timeout (default: 30s)' - type: string - url: - description: |- - URL is the endpoint to call when the tool is invoked - Required for plain HTTP tools. MCP actor-backed tools may omit it when - HTTP is present only for transport auth settings. - type: string - type: object - mcp: - description: |- - MCP defines a durable MCP server backend for this tool. MCP tools must set - substrateActor. - properties: - path: - description: |- - Path is the HTTP path exposed by the MCP server inside the actor. - Defaults to /mcp. - type: string - substrateActor: - description: SubstrateActor configures a durable Substrate actor - that hosts the MCP server. - properties: - boot: - description: Boot asks Substrate to boot this actor from scratch - on first resume. - type: boolean - poolRef: - description: PoolRef optionally assigns this tool actor to - an Orka SubstrateActorPool. - properties: - name: - description: Name is the pool name. - type: string - namespace: - description: Namespace is the pool namespace. It defaults - to the Task namespace. - type: string - type: object - templateRef: - description: TemplateRef references the ActorTemplate that - runs the MCP server. - properties: - name: - description: Name is the name of the workspace template. - type: string - namespace: - description: |- - Namespace is the namespace of the workspace template and claim. - It defaults to the Task namespace, or the controller namespace when configured. - type: string - type: object - required: - - templateRef - type: object - required: - - substrateActor - type: object - parameters: - description: Parameters is the JSON Schema for tool parameters (OpenAI - function calling format) - x-kubernetes-preserve-unknown-fields: true - required: - - description - type: object - x-kubernetes-validations: - - message: http or mcp.substrateActor is required - rule: has(self.http) || (has(self.mcp) && has(self.mcp.substrateActor)) - - message: http.url is required unless mcp.substrateActor is set - rule: '!has(self.http) || (has(self.mcp) && has(self.mcp.substrateActor)) - || (has(self.http.url) && self.http.url.size() > 0)' - status: - description: ToolStatus defines the observed state of Tool - properties: - actor: - description: Actor reports durable actor metadata when this tool is - MCP actor-backed. - properties: - actorID: - description: ActorID is the stable provider actor id. - type: string - poolRef: - description: PoolRef is the resolved Orka actor pool. - properties: - name: - description: Name is the pool name. - type: string - namespace: - description: Namespace is the pool namespace. It defaults - to the Task namespace. - type: string - type: object - provider: - description: Provider is the actor provider. - enum: - - agent-sandbox - - substrate - type: string - routeHost: - description: RouteHost is the HTTP Host value used with the provider - router. - type: string - templateRef: - description: TemplateRef is the resolved actor template. - properties: - name: - description: Name is the name of the workspace template. - type: string - namespace: - description: |- - Namespace is the namespace of the workspace template and claim. - It defaults to the Task namespace, or the controller namespace when configured. - type: string - type: object - type: object - available: - description: Available indicates whether the tool endpoint is reachable - type: boolean - conditions: - description: Conditions represent the current state of the Tool - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - endpoint: - description: Endpoint is the resolved non-secret endpoint used by - workers. - type: string - error: - description: Error is the error message if the tool is unavailable - type: string - lastCheck: - description: LastCheck is the timestamp of the last health check - format: date-time - type: string - required: - - available - type: object - type: object - served: true - storage: true - subresources: - status: {} + name: orka-container-worker + namespace: orka-system --- apiVersion: v1 kind: ServiceAccount metadata: labels: - app.kubernetes.io/component: agent-harness-wrapper app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-agent-harness-wrapper + name: orka-controller-manager namespace: orka-system --- apiVersion: v1 +automountServiceAccountToken: false kind: ServiceAccount metadata: - name: orka-ai-worker + labels: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka + name: orka-provider-auth-proxy namespace: orka-system --- apiVersion: v1 +automountServiceAccountToken: false kind: ServiceAccount metadata: - name: orka-container-worker + labels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + name: orka-scm-egress-proxy namespace: orka-system --- apiVersion: v1 kind: ServiceAccount metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-controller-manager + name: orka-vendor-worker namespace: orka-system --- apiVersion: v1 +automountServiceAccountToken: false kind: ServiceAccount metadata: - name: orka-vendor-worker + labels: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka + name: orka-workspace-publisher namespace: orka-system --- apiVersion: rbac.authorization.k8s.io/v1 @@ -6945,6 +251,7 @@ rules: - core.orka.ai resources: - tools + - outboundaccesspolicies verbs: - get - list @@ -7155,6 +462,167 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + workspace.orka.ai/aggregate-to-parameter-reader: "true" + name: orka-fake-workspace-parameter-reader +rules: +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakeproviderconfigs + - fakepoolparameters + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-fake.workspace-fakepoolparameters-admin-role +rules: +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakepoolparameters + verbs: + - '*' +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakepoolparameters/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-fake.workspace-fakepoolparameters-editor-role +rules: +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakepoolparameters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakepoolparameters/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-fake.workspace-fakepoolparameters-viewer-role +rules: +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakepoolparameters + verbs: + - get + - list + - watch +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakepoolparameters/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-fake.workspace-fakeproviderconfig-admin-role +rules: +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakeproviderconfigs + verbs: + - '*' +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakeproviderconfigs/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-fake.workspace-fakeproviderconfig-editor-role +rules: +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakeproviderconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakeproviderconfigs/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-fake.workspace-fakeproviderconfig-viewer-role +rules: +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakeproviderconfigs + verbs: + - get + - list + - watch +- apiGroups: + - fake.workspace.orka.ai + resources: + - fakeproviderconfigs/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize @@ -7273,6 +741,10 @@ rules: - "" resources: - configmaps + - namespaces + - pods + - secrets + - services verbs: - create - delete @@ -7285,14 +757,11 @@ rules: - "" resources: - endpoints - - namespaces - nodes - persistentvolumeclaims - persistentvolumes - - pods - pods/status - replicationcontrollers - - services verbs: - get - list @@ -7317,19 +786,9 @@ rules: - "" resources: - pods/portforward + - serviceaccounts/token verbs: - create -- apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - get - - list - - update - - watch - apiGroups: - "" resources: @@ -7363,12 +822,23 @@ rules: - apps resources: - daemonsets + - statefulsets + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: - deployments - replicasets - - statefulsets verbs: + - create + - delete - get - list + - patch + - update - watch - apiGroups: - ate.dev @@ -7378,6 +848,12 @@ rules: - get - list - watch +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create - apiGroups: - autoscaling resources: @@ -7423,9 +899,16 @@ rules: resources: - agentruntimes - agents + - branchclaims + - controllerepochs + - externaleffects + - promptattempts - providers + - publications - repositorymonitors - repositoryscans + - runtimepools + - runtimesessioncontrols - skills - substrateactorpools - tasks @@ -7443,9 +926,17 @@ rules: resources: - agentruntimes/finalizers - agents/finalizers + - branchclaims/finalizers + - controllerepochs/finalizers + - externaleffects/finalizers + - outboundaccesspolicies/finalizers + - promptattempts/finalizers - providers/finalizers + - publications/finalizers - repositorymonitors/finalizers - repositoryscans/finalizers + - runtimepools/finalizers + - runtimesessioncontrols/finalizers - skills/finalizers - substrateactorpools/finalizers - tasks/finalizers @@ -7457,9 +948,17 @@ rules: resources: - agentruntimes/status - agents/status + - branchclaims/status + - controllerepochs/status + - externaleffects/status + - outboundaccesspolicies/status + - promptattempts/status - providers/status + - publications/status - repositorymonitors/status - repositoryscans/status + - runtimepools/status + - runtimesessioncontrols/status - skills/status - substrateactorpools/status - tasks/status @@ -7468,6 +967,14 @@ rules: - get - patch - update +- apiGroups: + - core.orka.ai + resources: + - outboundaccesspolicies + verbs: + - get + - list + - watch - apiGroups: - discovery.k8s.io resources: @@ -7561,20 +1068,27 @@ rules: - delete - get - list + - patch + - update - watch - apiGroups: - policy resources: - poddisruptionbudgets verbs: + - create + - delete - get - list + - patch + - update - watch - apiGroups: - rbac.authorization.k8s.io resources: - clusterrolebindings - rolebindings + - roles verbs: - create - delete @@ -7582,6 +1096,14 @@ rules: - list - update - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - get + - list + - watch - apiGroups: - rbac.authorization.k8s.io resourceNames: @@ -7593,22 +1115,76 @@ rules: verbs: - bind - apiGroups: - - rbac.authorization.k8s.io + - storage.k8s.io resources: - - clusterroles - - roles + - storageclasses verbs: - get - list - watch - apiGroups: - - storage.k8s.io + - workspace.orka.ai resources: - - storageclasses + - executionworkspaceclasses verbs: + - create + - delete - get - list - - watch + - patch + - update + - use + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses/finalizers + - executionworkspaceproviders/finalizers + - executionworkspaces/finalizers + verbs: + - update +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses/status + - executionworkspacepools/status + - executionworkspaceproviders/status + - executionworkspaces/status + verbs: + - get + - patch + - update +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspacepools + verbs: + - get + - list + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceproviders + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaces + verbs: + - admit + - create + - delete + - get + - list + - patch + - update + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -7797,6 +1373,7 @@ rules: - core.orka.ai resources: - tools + - outboundaccesspolicies verbs: - get - list @@ -7968,6 +1545,255 @@ rules: - list --- apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspace-viewer-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaces + verbs: + - get + - list + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaces/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspaceclass-admin-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses + verbs: + - '*' +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspaceclass-editor-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspaceclass-viewer-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses + verbs: + - get + - list + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceclasses/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspacepool-admin-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspacepools + verbs: + - '*' +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspacepools/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspacepool-editor-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspacepools + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspacepools/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspacepool-viewer-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspacepools + verbs: + - get + - list + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspacepools/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspaceprovider-admin-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceproviders + verbs: + - '*' +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceproviders/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspaceprovider-editor-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceproviders/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-executionworkspaceprovider-viewer-role +rules: +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceproviders + verbs: + - get + - list + - watch +- apiGroups: + - workspace.orka.ai + resources: + - executionworkspaceproviders/status + verbs: + - get +--- +aggregationRule: + clusterRoleSelectors: + - matchLabels: + workspace.orka.ai/aggregate-to-parameter-reader: "true" +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: orka + name: orka-workspace-parameter-reader +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: @@ -8052,23 +1878,33 @@ subjects: name: orka-vendor-worker namespace: orka-system --- -apiVersion: v1 -kind: Service +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding metadata: labels: - app.kubernetes.io/component: agent-harness-wrapper app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-agent-harness-wrapper + name: orka-workspace-parameter-reader-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orka-workspace-parameter-reader +subjects: +- kind: ServiceAccount + name: orka-controller-manager + namespace: orka-system +--- +apiVersion: v1 +data: + ORKA_ACP_CLAUDE_RUNTIME_IMAGE: docker.io/sozercan/orka-acp-claude@sha256:0000000000000000000000000000000000000000000000000000000000000000 + ORKA_ACP_CODEX_RUNTIME_IMAGE: docker.io/sozercan/orka-acp-codex@sha256:0000000000000000000000000000000000000000000000000000000000000000 + ORKA_ACP_COPILOT_RUNTIME_IMAGE: docker.io/sozercan/orka-acp-copilot@sha256:0000000000000000000000000000000000000000000000000000000000000000 +kind: ConfigMap +metadata: + labels: + orka.ai/acp-runtime-images: "true" + name: acp-runtime-images-4d57d78k2k namespace: orka-system -spec: - ports: - - name: http - port: 8080 - targetPort: http - selector: - app.kubernetes.io/component: agent-harness-wrapper - app.kubernetes.io/name: orka --- apiVersion: v1 kind: Service @@ -8109,10 +1945,61 @@ spec: control-plane: controller-manager --- apiVersion: v1 -kind: PersistentVolumeClaim +kind: Service metadata: - name: orka-controller-manager-store - namespace: orka-system + labels: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka + name: orka-provider-auth-proxy + namespace: orka-system +spec: + ports: + - name: http + port: 8080 + targetPort: http + selector: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + name: orka-scm-egress-proxy + namespace: orka-system +spec: + ports: + - name: http-proxy + port: 8080 + targetPort: http-proxy + selector: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka + name: orka-workspace-publisher + namespace: orka-system +spec: + ports: + - name: http + port: 8080 + targetPort: http + selector: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: orka-controller-manager-store + namespace: orka-system spec: accessModes: - ReadWriteOnce @@ -8120,113 +2007,20 @@ spec: requests: storage: 1Gi --- -apiVersion: apps/v1 -kind: Deployment +apiVersion: v1 +kind: PersistentVolumeClaim metadata: labels: - app.kubernetes.io/component: agent-harness-wrapper - app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/component: workspace-publisher app.kubernetes.io/name: orka - name: orka-agent-harness-wrapper + name: orka-workspace-publisher namespace: orka-system spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/component: agent-harness-wrapper - app.kubernetes.io/name: orka - strategy: - type: Recreate - template: - metadata: - labels: - app.kubernetes.io/component: agent-harness-wrapper - app.kubernetes.io/name: orka - spec: - automountServiceAccountToken: false - containers: - - env: - - name: ORKA_HARNESS_WRAPPER_RUNTIME - value: multi - - name: ORKA_HARNESS_WRAPPER_LISTEN_ADDR - value: :8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_ALLOW_BASH - value: "true" - - name: ORKA_HARNESS_WRAPPER_CHILD_UID - value: "1000" - - name: ORKA_HARNESS_WRAPPER_CHILD_GID - value: "1000" - - name: ORKA_CODEX_SANDBOX_MODE - value: danger-full-access - - name: ORKA_SA_TOKEN_PATH - value: /var/run/orka/upload-token/token - image: ghcr.io/orka-agents/orka/agent-harness-wrapper:0.1.1 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 10 - periodSeconds: 20 - name: wrapper - ports: - - containerPort: 8080 - name: http - readinessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - limits: - cpu: "2" - memory: 2Gi - requests: - cpu: 100m - memory: 256Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - add: - - SETUID - - SETGID - - CHOWN - - KILL - - FOWNER - drop: - - ALL - readOnlyRootFilesystem: true - volumeMounts: - - mountPath: /var/run/orka/harness-wrapper - name: auth - readOnly: true - - mountPath: /var/run/orka/upload-token - name: upload-token - readOnly: true - - mountPath: /tmp - name: tmp - securityContext: - runAsGroup: 0 - runAsUser: 0 - seccompProfile: - type: RuntimeDefault - serviceAccountName: orka-agent-harness-wrapper - volumes: - - name: upload-token - projected: - defaultMode: 256 - sources: - - serviceAccountToken: - path: token - - name: auth - secret: - defaultMode: 256 - secretName: harness-wrapper-auth - - emptyDir: {} - name: tmp + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi --- apiVersion: apps/v1 kind: Deployment @@ -8261,6 +2055,16 @@ spec: - --store-backend=sqlite - --store-path=/data/orka.db - --controller-url=http://orka-api.orka-system.svc:8080 + - --acp-runtime-enabled=true + - --acp-upgrade-drain-bind-address=127.0.0.1:8083 + - --acp-upgrade-drain-timeout=5m + - --acp-upgrade-drain-poll-interval=1s + - --acp-upgrade-drain-trigger-timeout=5m15s + - --acp-upgrade-drain-marker-namespace=orka-system + - --acp-provider-proxy-base-url=http://orka-provider-auth-proxy.orka-system.svc:8080 + - --acp-provider-proxy-namespace=orka-system + - --acp-provider-proxy-pod-labels=orka.ai/network-role=provider-auth-proxy + - --acp-provider-proxy-token-file=/var/run/orka/provider-auth/token - --ai-worker-image=ghcr.io/orka-agents/orka/ai-worker:0.1.1 - --general-worker-image=ghcr.io/orka-agents/orka/general-worker:0.1.1 - --gateway-enabled=true @@ -8288,14 +2092,40 @@ spec: command: - /manager env: - - name: ORKA_HARNESS_WRAPPER_ENDPOINT - value: http://orka-agent-harness-wrapper:8080 - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper/token - - name: ORKA_HARNESS_WRAPPER_SERVICE_ACCOUNT_NAME - value: orka-agent-harness-wrapper - image: ghcr.io/orka-agents/orka:0.1.1 + - name: ORKA_ACP_ARTIFACT_CAPABILITY_SECRET_FILE + value: /var/run/orka/acp-artifacts/capability-secret + - name: ORKA_ACP_ARTIFACT_ROOT + value: /data/acp-artifacts + - name: ORKA_ACP_CODEX_RUNTIME_IMAGE + valueFrom: + configMapKeyRef: + key: ORKA_ACP_CODEX_RUNTIME_IMAGE + name: acp-runtime-images-4d57d78k2k + - name: ORKA_ACP_CLAUDE_RUNTIME_IMAGE + valueFrom: + configMapKeyRef: + key: ORKA_ACP_CLAUDE_RUNTIME_IMAGE + name: acp-runtime-images-4d57d78k2k + - name: ORKA_ACP_COPILOT_RUNTIME_IMAGE + valueFrom: + configMapKeyRef: + key: ORKA_ACP_COPILOT_RUNTIME_IMAGE + name: acp-runtime-images-4d57d78k2k + - name: ORKA_WORKSPACE_PUBLISHER_URL + value: http://orka-workspace-publisher.orka-system.svc:8080 + - name: ORKA_WORKSPACE_PUBLISHER_CONTROLLER_TOKEN_FILE + value: /var/run/orka/publisher-auth/controller-token + - name: ORKA_WORKSPACE_PUBLISHER_CAPABILITY_SECRET_FILE + value: /var/run/orka/publisher-auth/operation-capability-secret + image: docker.io/sozercan/orka@sha256:0000000000000000000000000000000000000000000000000000000000000000 imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /manager + - --acp-upgrade-drain-trigger-url=http://127.0.0.1:8083/acp/upgrade-drain + - --acp-upgrade-drain-trigger-timeout=5m15s livenessProbe: httpGet: path: /healthz @@ -8328,8 +2158,14 @@ spec: name: store - mountPath: /tmp name: tmp - - mountPath: /var/run/orka/harness-wrapper - name: harness-wrapper-auth + - mountPath: /var/run/orka/acp-artifacts + name: acp-artifact-capability + readOnly: true + - mountPath: /var/run/orka/publisher-auth + name: workspace-publisher-auth + readOnly: true + - mountPath: /var/run/orka/provider-auth + name: provider-auth-proxy readOnly: true securityContext: fsGroup: 65532 @@ -8337,16 +2173,452 @@ spec: seccompProfile: type: RuntimeDefault serviceAccountName: orka-controller-manager - terminationGracePeriodSeconds: 10 + terminationGracePeriodSeconds: 360 volumes: - name: store persistentVolumeClaim: claimName: orka-controller-manager-store - emptyDir: {} name: tmp - - name: harness-wrapper-auth + - name: acp-artifact-capability + secret: + defaultMode: 256 + items: + - key: capability-secret + path: capability-secret + secretName: acp-artifact-capability + - name: workspace-publisher-auth + secret: + defaultMode: 256 + secretName: workspace-publisher-auth + - name: provider-auth-proxy + secret: + defaultMode: 256 + items: + - key: token + path: token + secretName: provider-auth-proxy +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka + name: orka-provider-auth-proxy + namespace: orka-system +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka + strategy: + type: Recreate + template: + metadata: + annotations: + orka.ai/provider-auth-rollout-nonce: "0" + labels: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka + orka.ai/network-role: provider-auth-proxy + spec: + automountServiceAccountToken: false + containers: + - args: + - --listen-address=:8080 + - --upstream-base-url=http://vekil.vekil-system.svc:1337 + - --token-file=/var/run/secrets/orka/provider-auth/token + - --previous-token-file=/var/run/secrets/orka/provider-auth/previous-token + - --previous-token-valid-until-file=/var/run/secrets/orka/provider-auth/previous-token-valid-until + - --token-reload-interval=5s + - --previous-token-overlap=10m + command: + - /provider-auth-proxy + image: docker.io/sozercan/orka@sha256:0000000000000000000000000000000000000000000000000000000000000000 + livenessProbe: + httpGet: + path: /healthz + port: http + name: proxy + ports: + - containerPort: 8080 + name: http + readinessProbe: + httpGet: + path: /readyz + port: http + resources: + limits: + cpu: 250m + ephemeral-storage: 128Mi + memory: 128Mi + requests: + cpu: 25m + ephemeral-storage: 32Mi + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /var/run/secrets/orka/provider-auth + name: provider-auth + readOnly: true + securityContext: + fsGroup: 65532 + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + serviceAccountName: orka-provider-auth-proxy + volumes: + - name: provider-auth + projected: + defaultMode: 288 + sources: + - secret: + items: + - key: token + path: token + - key: previous-token + path: previous-token + - key: previous-token-valid-until + path: previous-token-valid-until + name: provider-auth-proxy + optional: true +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + name: orka-scm-egress-proxy + namespace: orka-system +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + strategy: + type: Recreate + template: + metadata: + labels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + orka.ai/network-role: scm-egress-proxy + spec: + automountServiceAccountToken: false + containers: + - args: + - --listen-address=:8080 + - --allowed-hosts=github.com + - --forge-api-base-url=https://api.github.com + - --token-file=/var/run/secrets/orka/scm-egress/token + - --max-request-header-bytes=32768 + - --max-response-header-bytes=65536 + - --max-request-bytes=4194304 + - --max-response-bytes=8388608 + - --max-tunnel-bytes=1073741824 + - --max-concurrent=8 + - --resolution-timeout=5s + - --connect-timeout=10s + - --response-header-timeout=30s + - --forward-timeout=2m + - --idle-timeout=30s + - --tunnel-timeout=10m + command: + - /scm-egress-proxy + image: docker.io/sozercan/orka@sha256:0000000000000000000000000000000000000000000000000000000000000000 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /healthz + port: http-proxy + name: proxy + ports: + - containerPort: 8080 + name: http-proxy + readinessProbe: + httpGet: + path: /readyz + port: http-proxy + resources: + limits: + cpu: 500m + ephemeral-storage: 128Mi + memory: 256Mi + requests: + cpu: 25m + ephemeral-storage: 32Mi + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /var/run/secrets/orka/scm-egress/token + name: auth + readOnly: true + subPath: token + securityContext: + fsGroup: 65532 + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + serviceAccountName: orka-scm-egress-proxy + volumes: + - name: auth + secret: + defaultMode: 288 + items: + - key: token + path: token + secretName: scm-egress-proxy-auth +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka + name: orka-workspace-publisher + namespace: orka-system +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka + strategy: + type: Recreate + template: + metadata: + labels: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka + orka.ai/network-role: workspace-publisher + spec: + automountServiceAccountToken: false + containers: + - env: + - name: ORKA_SCM_EGRESS_PROXY_TOKEN + valueFrom: + secretKeyRef: + key: token + name: scm-egress-proxy-auth + - name: HTTPS_PROXY + value: http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@orka-scm-egress-proxy.orka-system.svc:8080 + - name: https_proxy + value: http://orka-publisher:$(ORKA_SCM_EGRESS_PROXY_TOKEN)@orka-scm-egress-proxy.orka-system.svc:8080 + - name: NO_PROXY + value: localhost,127.0.0.1,::1,.svc,.cluster.local + - name: no_proxy + value: localhost,127.0.0.1,::1,.svc,.cluster.local + - name: ORKA_PUBLISHER_SCM_EGRESS_PROXY_REQUIRED + value: "true" + - name: ORKA_PUBLISHER_LISTEN_ADDRESS + value: :8080 + - name: ORKA_PUBLISHER_TEMP_ROOT + value: /tmp/orka-workspace-publisher/runtime + - name: ORKA_PUBLISHER_CONTROLLER_TOKEN_FILE + value: /var/run/orka/publisher-auth/controller-token + - name: ORKA_PUBLISHER_OPERATION_CAPABILITY_SECRET_FILE + value: /var/run/orka/publisher-auth/operation-capability-secret + - name: ORKA_PUBLISHER_ARTIFACT_AUTHORIZATION_BROKER_URL + value: http://orka-api.orka-system.svc:8080 + - name: ORKA_PUBLISHER_ARTIFACT_API_URL + value: http://orka-api.orka-system.svc:8080 + - name: ORKA_PUBLISHER_CREDENTIAL_BROKER_URL + value: http://orka-api.orka-system.svc:8080 + - name: ORKA_PUBLISHER_ALLOWED_SCM_HOSTS + value: github.com + - name: ORKA_PUBLISHER_GITHUB_PR_ENABLED + value: "true" + - name: ORKA_PUBLISHER_GITHUB_API_BASE_URL + value: https://api.github.com + - name: ORKA_PUBLISHER_GITHUB_REQUEST_TIMEOUT + value: 15s + - name: ORKA_PUBLISHER_GITHUB_MAX_RESPONSE_BYTES + value: "4194304" + image: docker.io/sozercan/orka-workspace-publisher@sha256:0000000000000000000000000000000000000000000000000000000000000000 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /v1/health + port: http + periodSeconds: 10 + name: publisher + ports: + - containerPort: 8080 + name: http + readinessProbe: + httpGet: + path: /v1/health + port: http + periodSeconds: 5 + resources: + limits: + cpu: "2" + ephemeral-storage: 2Gi + memory: 2Gi + requests: + cpu: 100m + ephemeral-storage: 512Mi + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /data + name: data + - mountPath: /tmp/orka-workspace-publisher + name: tmp + - mountPath: /var/run/orka/publisher-auth/controller-token + name: publisher-auth + readOnly: true + subPath: controller-token + - mountPath: /var/run/orka/publisher-auth/operation-capability-secret + name: publisher-auth + readOnly: true + subPath: operation-capability-secret + securityContext: + fsGroup: 65532 + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + serviceAccountName: orka-workspace-publisher + volumes: + - name: data + persistentVolumeClaim: + claimName: orka-workspace-publisher + - emptyDir: + sizeLimit: 1Gi + name: tmp + - name: publisher-auth secret: - secretName: harness-wrapper-auth + defaultMode: 288 + items: + - key: controller-token + path: controller-token + - key: operation-capability-secret + path: operation-capability-secret + secretName: workspace-publisher-auth +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: orka-executionworkspace-core-admission.orka.ai + namespace: orka-system +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - workspace.orka.ai + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - executionworkspaces + validations: + - expression: |- + !has(object.spec.coreAdmission) || (oldObject != null && has(oldObject.spec.coreAdmission) && + + object.spec.coreAdmission.admittedGeneration == oldObject.spec.coreAdmission.admittedGeneration && + has(object.spec.coreAdmission.poolBinding) == has(oldObject.spec.coreAdmission.poolBinding) && + (!has(object.spec.coreAdmission.poolBinding) || + (object.spec.coreAdmission.poolBinding.name == oldObject.spec.coreAdmission.poolBinding.name && + object.spec.coreAdmission.poolBinding.uid == oldObject.spec.coreAdmission.poolBinding.uid && + object.spec.coreAdmission.poolBinding.generation == oldObject.spec.coreAdmission.poolBinding.generation && + has(object.spec.coreAdmission.poolBinding.profileHash) == has(oldObject.spec.coreAdmission.poolBinding.profileHash) && + (!has(object.spec.coreAdmission.poolBinding.profileHash) || + object.spec.coreAdmission.poolBinding.profileHash == oldObject.spec.coreAdmission.poolBinding.profileHash)))) || + authorizer.group('workspace.orka.ai') + + .resource('executionworkspaces') + .namespace(request.namespace) + .name(object.metadata.name) + .check('admit').allowed() + message: only the Orka core controller may establish or advance ExecutionWorkspace + core admission + reason: Forbidden +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: orka-task-workspace-class-use.orka.ai + namespace: orka-system +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - core.orka.ai + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - tasks + validations: + - expression: |- + !has(object.spec.execution) || !has(object.spec.execution.workspace) || !has(object.spec.execution.workspace.classRef) || authorizer.group('workspace.orka.ai') + + .resource('executionworkspaceclasses') + .namespace(request.namespace) + .name(object.spec.execution.workspace.classRef.name) + .check('use').allowed() + message: caller is not authorized to use the selected ExecutionWorkspaceClass + reason: Forbidden +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: orka-tool-workspace-class-use.orka.ai + namespace: orka-system +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - core.orka.ai + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - tools + validations: + - expression: |- + !has(object.spec.mcp) || !has(object.spec.mcp.workspace) || authorizer.group('workspace.orka.ai') + + .resource('executionworkspaceclasses') + .namespace(request.namespace) + .name(object.spec.mcp.workspace.classRef.name) + .check('use').allowed() + message: caller is not authorized to use the selected ExecutionWorkspaceClass + reason: Forbidden --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy @@ -8403,9 +2675,245 @@ spec: --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding +metadata: + name: orka-executionworkspace-core-admission.orka.ai + namespace: orka-system +spec: + policyName: orka-executionworkspace-core-admission.orka.ai + validationActions: + - Deny +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: orka-task-workspace-class-use.orka.ai + namespace: orka-system +spec: + policyName: orka-task-workspace-class-use.orka.ai + validationActions: + - Deny +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: orka-tool-workspace-class-use.orka.ai + namespace: orka-system +spec: + policyName: orka-tool-workspace-class-use.orka.ai + validationActions: + - Deny +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding metadata: name: orka-gateway-task-protection spec: policyName: orka-gateway-task-protection validationActions: - Deny +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka + name: orka-provider-auth-proxy + namespace: orka-system +spec: + egress: + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + - ports: + - port: 1337 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: vekil-system + podSelector: + matchLabels: + app.kubernetes.io/name: vekil + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: orka-runtimes + podSelector: + matchLabels: + orka.ai/network-role: provider-client + ports: + - port: 8080 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/component: provider-auth-proxy + app.kubernetes.io/name: orka + policyTypes: + - Ingress + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + name: orka-scm-egress-proxy + namespace: orka-system +spec: + egress: + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + - ports: + - port: 443 + protocol: TCP + to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 0.0.0.0/8 + - 10.0.0.0/8 + - 100.64.0.0/10 + - 127.0.0.0/8 + - 169.254.0.0/16 + - 172.16.0.0/12 + - 192.0.0.0/24 + - 192.0.2.0/24 + - 192.168.0.0/16 + - 198.18.0.0/15 + - 198.51.100.0/24 + - 203.0.113.0/24 + - 224.0.0.0/4 + - 240.0.0.0/4 + - ports: + - port: 443 + protocol: TCP + to: + - ipBlock: + cidr: ::/0 + except: + - ::/128 + - ::1/128 + - 64:ff9b::/96 + - 64:ff9b:1::/48 + - 100::/64 + - 2001::/32 + - 2001:db8::/32 + - 2002::/16 + - fc00::/7 + - fe80::/10 + - ff00::/8 + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka + ports: + - port: 8080 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + policyTypes: + - Ingress + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: orka-workspace-publisher + namespace: orka-system +spec: + egress: + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + - ports: + - port: 8080 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/name: orka + control-plane: controller-manager + - ports: + - port: 8080 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/name: orka + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: orka + control-plane: controller-manager + ports: + - port: 8080 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/name: orka + policyTypes: + - Ingress + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/managed-by: orka + name: orka-provider-auth-proxy-only + namespace: vekil-system +spec: + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: orka-system + podSelector: + matchLabels: + orka.ai/network-role: provider-auth-proxy + ports: + - port: 1337 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/name: vekil + policyTypes: + - Ingress diff --git a/docs/adr/0001-execution-workspace-default-provider.md b/docs/adr/0001-execution-workspace-default-provider.md index 41d60d268..db10e919a 100644 --- a/docs/adr/0001-execution-workspace-default-provider.md +++ b/docs/adr/0001-execution-workspace-default-provider.md @@ -1,5 +1,9 @@ -# Use an explicit default provider for Execution Workspaces +# ADR 0001: Use an explicit default provider for Execution Workspaces -When a Task requests an Execution Workspace without setting `spec.execution.workspace.provider`, Orka resolves the provider from an operator-configured Default Workspace Provider and falls back to `agent-sandbox` for compatibility. Orka does not infer the provider from installed cluster components because ambient detection is ambiguous when multiple providers are installed, stale CRDs remain, or RBAC hides provider resources. +## Status -Standard Worker Execution is not a provider. Tasks that do not request an Execution Workspace keep the existing direct Kubernetes worker Job path. +Superseded by the ACP core RuntimePool cutover. + +The earlier API allowed a Task to select an execution-workspace provider through `spec.execution.workspace.provider`. The current built-in agent path rejects `Task.spec.execution.workspace` and never auto-detects or defaults an upstream provider. + +Current agent repository input and publication policy belongs at top-level `Task.spec.workspace`. Future agent-sandbox or Substrate integration must be explicit, operator-configured, and implemented behind the `orka.harness.v2` RuntimeSession lifecycle; ambient provider discovery remains disallowed. diff --git a/docs/adr/0002-provider-neutral-execution-workspace-status.md b/docs/adr/0002-provider-neutral-execution-workspace-status.md index 2fa5f0e84..7e4b3f6ff 100644 --- a/docs/adr/0002-provider-neutral-execution-workspace-status.md +++ b/docs/adr/0002-provider-neutral-execution-workspace-status.md @@ -1,5 +1,13 @@ -# Report provider-neutral Execution Workspace status +# ADR 0002: Report provider-neutral Execution Workspace status -Tasks that use an Execution Workspace should report a safe, provider-neutral lifecycle summary instead of exposing provider-native objects such as Substrate Actor snapshots or daemon URLs. Workers should emit workspace lifecycle updates through a small authenticated internal Task status endpoint because workers own provider lifecycle operations in the wrapper-first model. The controller records validation and lock failures directly, but it should not poll provider-native resources for normal workspace progress. +## Status -Intermediate workspace status updates are best effort. The Task result path remains the source of command completion, while requested cleanup state must still be reached for a workspace-backed Task to succeed. +Superseded for built-in agent Tasks by ACP v2 execution and delivery status. + +The earlier prototype reported worker-owned upstream workspace lifecycle. Current ACP agent Tasks expose provider-neutral control state through: + +- `Task.status.execution` for the fenced attempt, RuntimePool, RuntimeSession, prompt, and outcome; +- `Task.status.delivery` for workspace validation, clean-room publication, verification, and PR receipts; +- `RuntimePool.status` for lifecycle, admission, exact instance, capacity, and pressure. + +Future execution-workspace providers must project into these Orka-owned surfaces and must not expose provider-native snapshot URIs, daemon URLs, credentials, or mutable child-controlled Git state. diff --git a/docs/adr/0003-use-provider-route-for-workspace-daemon.md b/docs/adr/0003-use-provider-route-for-workspace-daemon.md index bf8346ede..3920f701d 100644 --- a/docs/adr/0003-use-provider-route-for-workspace-daemon.md +++ b/docs/adr/0003-use-provider-route-for-workspace-daemon.md @@ -1,3 +1,9 @@ -# Use the provider route for Workspace Daemon calls +# ADR 0003: Use the provider route for Workspace Daemon calls -For Substrate-backed Execution Workspaces, Orka workers should call the Workspace Daemon through Substrate's router and actor DNS route instead of calling provider-native worker pod IPs directly. This keeps Orka coupled to the provider's stable routing abstraction rather than transient actor placement details. +## Status + +Deferred with the Substrate ACP integration. + +The earlier worker-based Substrate prototype used the provider router and Actor DNS route instead of transient worker Pod IPs. The current ACP RuntimePool path does not call a Substrate workspace daemon. + +If an Actor-backed `orka.harness.v2` supervisor is implemented, it should still use the provider's stable authenticated route rather than provider-native Pod placement details. That routing choice must not weaken exact runtime-instance fencing or expose Git/provider credentials to the wrong process tree. diff --git a/docs/adr/0004-use-minimal-substrate-control-client.md b/docs/adr/0004-use-minimal-substrate-control-client.md index 5637de7a2..2fecda954 100644 --- a/docs/adr/0004-use-minimal-substrate-control-client.md +++ b/docs/adr/0004-use-minimal-substrate-control-client.md @@ -1,3 +1,7 @@ -# Use a minimal Substrate control API client +# ADR 0004: Use a minimal Substrate control API client -Orka should generate or vendor only the small Substrate API surface needed for actor lifecycle operations instead of importing the full `github.com/agent-substrate/substrate` Go module. Orka needs narrow gRPC clients for the public control API and, when implementing checkpoint/restore features, the specific Ateom/AteomHerder checkpoint services. Avoiding the full module reduces Kubernetes dependency churn and cloud-provider transitive dependencies while still keeping capability-specific proto clients available inside `internal/substratepb`. +## Status + +Deferred with the Substrate ACP integration. + +A future Actor-backed RuntimeSession provider should continue to generate or vendor only the narrow Substrate lifecycle API surface it needs, rather than importing the full Substrate module. Any checkpoint/restore client must remain capability-specific and must not imply ACP prompt replay, provider-session restore, or publication from an Actor-controlled workspace. diff --git a/docs/adr/0005-require-explicit-substrate-api-trust.md b/docs/adr/0005-require-explicit-substrate-api-trust.md index 91b3d371d..cbc5c701a 100644 --- a/docs/adr/0005-require-explicit-substrate-api-trust.md +++ b/docs/adr/0005-require-explicit-substrate-api-trust.md @@ -1,5 +1,9 @@ -# Require explicit trust for the Substrate control API +# ADR 0005: Require explicit trust for the Substrate control API -Orka should connect to the Substrate control API with explicit TLS trust configuration instead of normalizing insecure TLS verification. Local kind setups may opt into insecure mode for development, but production configuration should provide a CA or equivalent trust material for lifecycle calls that create, resume, suspend, and delete Substrate Actors. +## Status -The Substrate API trust setting belongs to provider installation/configuration, not to individual Tasks. Tasks select a Workspace Provider and Template; they do not choose whether provider control-plane TLS is trusted. +Accepted as a requirement for any future Substrate ACP provider; not active in the current Kubernetes RuntimePool path. + +A future integration must connect to Substrate with explicit TLS trust material. Local kind evaluation may opt into insecure verification, but production configuration must provide a reviewed CA or equivalent trust anchor for Actor lifecycle calls. + +Trust configuration belongs to the operator-managed provider/runtime profile, never an individual Task. Tasks do not choose whether control-plane TLS is trusted. diff --git a/docs/adr/0006-use-wrapper-first-execution-workspace-providers.md b/docs/adr/0006-use-wrapper-first-execution-workspace-providers.md index 7db4d5d0c..8e493bbb2 100644 --- a/docs/adr/0006-use-wrapper-first-execution-workspace-providers.md +++ b/docs/adr/0006-use-wrapper-first-execution-workspace-providers.md @@ -1,11 +1,17 @@ -# Use wrapper-first Execution Workspace providers +# ADR 0006: Use wrapper-first Execution Workspace providers Date: 2026-05-21 ## Status -Superseded as the target architecture by ADR 0012 and ADR 0014. Retained as historical and transitional context for the legacy direct-provider path. +Superseded by the ACP core RuntimePool cutover. -Orka should add new Execution Workspace providers behind the existing worker-wrapper path before moving provider lifecycle ownership into the controller. The controller validates, locks, and creates the normal worker Job; the outer worker claims the selected provider, stages the inner worker, runs it inside the workspace, reports provider-neutral status, and applies cleanup. This preserves Orka's existing worker auth, result submission, artifact upload, and agent runtime behavior while isolating provider-specific lifecycle code behind `WorkspaceExecutor`. +## Original decision -Controller-direct execution can be reconsidered after Substrate is stable enough to justify tighter lifecycle orchestration and stronger controller-owned observability. +The original execution-workspace prototype placed provider ownership in a per-Task worker path: the controller created a worker Job, the worker claimed an upstream workspace, staged another worker process, and handled result submission and cleanup. + +## Superseding decision + +Built-in `type: agent` Tasks now use only `orka.harness.v2` RuntimePools and private RuntimeSessions. There is no per-Task agent Job or worker-based fallback. Top-level `Task.spec.workspace` defines verified repository input and clean-room publication policy; `Task.spec.execution.workspace` is rejected by the ACP core runtime. + +Future agent-sandbox or Substrate support must host one RuntimeSession behind the same v2 lifecycle. Orka remains authoritative for attempt/session fences, prompt leases, cancellation, transcript/finalization, workspace deltas, publication, and delivery receipts. Source-read, target-read, target-write, and forge credentials remain outside the ACP process tree. diff --git a/docs/adr/0007-substrate-actor-pool-oversubscription.md b/docs/adr/0007-substrate-actor-pool-oversubscription.md index cf7618b1f..b5dcd7f07 100644 --- a/docs/adr/0007-substrate-actor-pool-oversubscription.md +++ b/docs/adr/0007-substrate-actor-pool-oversubscription.md @@ -1,9 +1,11 @@ -# Model Substrate oversubscription as controller-owned actor pools +# ADR 0007: Model Substrate oversubscription as controller-owned actor pools -Substrate oversubscription should be exposed through an Orka-owned actor pool abstraction rather than by overloading per-Task workspace fields. A pool represents a bounded Substrate WorkerPool plus a target actor density, such as 250 stateful actors across 8 workers, and the controller reconciles pool membership, queued claims, and cleanup pressure against that budget. Tasks still select an Execution Workspace provider and template; they may reference a pool, but they should not choose individual workers or actor pods. +## Status -The pool controller should own scheduling and juggling decisions. It can pre-create or retain suspended actors, resume actors when workers have capacity, suspend idle actors to free workers, and use Substrate `ListActors` and `ListWorkers` to publish density and placement health. Workers continue to own command execution through the wrapper-first path, so result submission, artifact upload, token handling, and provider-neutral Task status stay unchanged. +Deferred and superseded in part by the ACP RuntimePool model. -The first implementation slice should keep Task status provider-neutral: surface density, placement, and latency, but do not expose raw Substrate snapshot URIs, daemon URLs, or tokens. Later snapshot restore work can reuse the vendored checkpoint/restore clients, but restoring arbitrary snapshots should be a pool/controller action with explicit template compatibility checks instead of a worker-side shortcut. +The current first-release pool is a controller-owned logical ACP `RuntimePool` with one active Kubernetes Pod, bounded resident RuntimeSessions, bounded concurrent prompts, drain, and scale-to-zero behavior. Tasks do not choose individual workers or Pods. -This keeps oversubscription an operator-controlled capacity feature, avoids encoding provider-native pod choices into user Tasks, and leaves room for MCP tool-actors to reuse the same pool machinery when Orka adds durable tool-hosting actors. +A future Substrate implementation may map that logical pool to a bounded WorkerPool and Actor density, but each Actor must host one fenced `orka.harness.v2` RuntimeSession. Orka remains authoritative for queueing, admission, prompt leases, outcome classification, validation, publication reservations, and cleanup. Actor suspension or juggling must not occur while prompt, validation, publication, finalization, or Session lease work is active. + +Provider-native density and placement may be summarized safely in pool status. Raw snapshot URIs, daemon URLs, tokens, and arbitrary restore controls must not appear in Task status. diff --git a/docs/adr/0008-runtime-session-internal-store-first.md b/docs/adr/0008-runtime-session-internal-store-first.md index f5e518a7d..ddded1be8 100644 --- a/docs/adr/0008-runtime-session-internal-store-first.md +++ b/docs/adr/0008-runtime-session-internal-store-first.md @@ -4,25 +4,41 @@ Date: 2026-06-11 ## Status -Accepted for the first frontier implementation wave. +Superseded for ACP control authority by the Kubernetes hard cutover. Retained as +the historical record for why RuntimeSession was not initially exposed as a +public process-management CRD. ## Context -The remaining frontier introduces backend-neutral runtime sessions that can be claimed, reused, released, retained, suspended, and deleted. The lifecycle must support non-Substrate providers first while keeping Agent Substrate optional. Runtime sessions require strict namespace ownership and cleanup semantics, but the first provider still needs to prove the turn protocol and conformance suite before operators need a public CRD surface. +The remaining frontier introduces backend-neutral runtime sessions that can be claimed, reused, released, retained, suspended, and deleted. The lifecycle must support non-Substrate providers first while keeping Agent Substrate optional. Runtime sessions require strict namespace ownership, exact instance fencing, and cleanup semantics. The Kubernetes RuntimePool provider and v2 conformance suite now supply the first implementation seam without requiring a public RuntimeSession CRD. ## Decision -Start with an internal RuntimeSession state model and persistence boundary, then add a CRD only after the non-Substrate provider and cleanup loop have stable status requirements. +The original decision was to start with an internal RuntimeSession state model +and persistence boundary, then add a CRD only after the non-Substrate provider +and cleanup loop had stable status requirements. -The frozen state machine lives in `internal/harness` so the controller/provider implementation can share validation. Public API/CLI visibility can read from the internal store initially and later migrate to a CRD-backed implementation without changing the turn protocol. +The frozen state machine still lives in `internal/harness/v2` so the controller +and supervisor share validation. The hard cutover now makes +`RuntimeSessionControl` and the other ACP control CRDs authoritative through +status `resourceVersion` CAS, with coordination Leases for controller epoch and +session mutation ownership. SQLite remains the payload store for transcripts, +SessionTurns, deferred outbox projections, and artifacts. ## Consequences -- Provider integration can ship behind feature gates without expanding the Kubernetes API surface prematurely. -- Namespace ownership, cleanup policies, and transition validation are testable before persistence is introduced. -- Operator visibility is initially API/CLI-driven rather than `kubectl get runtimesessions`. -- A future CRD migration must preserve IDs, owner metadata, cleanup policy, active task, provider, phase, idle timeout, and max lifetime. +- Provider-private process details remain inside the runtime; the public control + record stores only Orka-owned lifecycle, fences, leases, and safe receipts. +- Namespace ownership, cleanup policy, and transition validation are enforced + by Kubernetes-authoritative records rather than SQLite control rows. +- Operator visibility is available through Task/RuntimePool status, the API/CLI, + and the ACP control CRDs. +- Cross-store finalization must preserve session UID/generation, + pool/runtime/controller fences, the active attempt/prompt, publication state, + transcript continuity, and the exact deferred outbox projection. ## Revisit -Revisit once the non-Substrate provider passes conformance and the cleanup controller needs watch/reconcile semantics that are awkward for an internal store. +Any future public process-detail API must remain a projection of this authority. +It must not introduce prompt replay, split authority between SQLite and +Kubernetes, or weaken v2 duplicate/fencing rules. diff --git a/docs/adr/0009-agentkit-byoa-harness-runtime-boundary.md b/docs/adr/0009-agentkit-byoa-harness-runtime-boundary.md deleted file mode 100644 index 610a7ce40..000000000 --- a/docs/adr/0009-agentkit-byoa-harness-runtime-boundary.md +++ /dev/null @@ -1,51 +0,0 @@ -# ADR 0009: Register bring-your-own agent runtimes through AgentRuntime - -Date: 2026-06-26 - -## Status - -Accepted for the first bring-your-own agent runtime implementation slice. - -## Context - -Orka routes `type: agent` tasks through `AgentRuntime`, an Orka-facing runtime contract backed by `orka.harness.v1`. The remote execution backend may be a generic self-hosted HTTP runtime, AgentKit Serve, Azure AI Foundry hosted agents, or a future backend. Orka should not parse backend authoring formats or accept arbitrary per-Agent images and commands. - -Backend-specific protocol skins are adapter-owned and live outside the Orka source tree. Orka keeps only the provider-neutral protocol, public Go aliases, conformance checks, reference fixtures, and facade samples. Orka needs a stable Kubernetes-native boundary: the `AgentRuntime` registry, readiness/conformance, runtime routing, task provenance, capability tiers, and brokered-governance hooks. - -## Decision - -Orka consumes only the `orka.harness.v1` endpoint for bring-your-own agent runtimes. - -For the first milestone, Orka supports `AgentRuntime.spec.deployment.mode: external-endpoint`. The runtime is pre-deployed outside the `AgentRuntime` controller, and Orka validates the endpoint before any `Task` may route to it. Managed adapter Deployments are explicitly deferred until the protocol seam is proven. - -`AgentRuntime` is **namespaced in this first slice**. Issue #160 describes the long-term cluster-scoped, admin-owned registry, but the external-endpoint milestone intentionally mirrors the existing namespaced `Provider` governance pattern so namespace owners can validate the protocol seam without introducing cluster-wide admission or runtime allowlists in the same PR. Admin governance is still required for Secret use: harness bearer-token Secrets must explicitly opt in with `orka.ai/agent-runtime-auth: "true"`, may scope themselves to a single runtime with `orka.ai/agent-runtime-name`, and must bind themselves to the intended endpoint with `orka.ai/agent-runtime-endpoint`. A future follow-up can add a cluster-scoped registry or namespace allowlist once managed/dedicated-image runtimes and the BYO trust tier are designed. - -Agents select remote execution backends with `spec.runtime.runtimeRef`. Built-in CLI runtimes continue to use `spec.runtime.type: codex|claude|copilot` and the shared harness-wrapper endpoint. An Agent must set exactly one of `type` or `runtimeRef`. - -Tool execution has capability tiers: - -- **Observed mode:** the remote execution backend may use its own internal tools; Orka records lifecycle, output, and terminal result from harness frames, but cannot govern backend-internal side effects. -- **Brokered mode:** brokered governance is the target model. Remote backends request Orka Tool execution, and Orka owns authorization, approval, idempotency-key injection, credential resolution, execution/brokering, events, lineage, and audit. Remote backends must not receive production Orka Tool credentials. - -Fibey remains one acceptance scenario, but the demo should show the same Orka API and approval UX across generic HTTP, AgentKit Serve, and Foundry-backed `AgentRuntime` facades. Foundry and AgentKit are backend options, not the architecture. - -## Consequences - -- Orka has a first-class `AgentRuntime` registry and readiness condition without exposing arbitrary Agent image/command fields. -- `AgentRuntime` status carries sanitized observed capabilities and `observedGeneration`, which lets task routing fail closed on non-ready or stale runtime definitions. -- Harness bearer token Secrets must opt in with `orka.ai/agent-runtime-auth: "true"`, may scope themselves to one runtime with `orka.ai/agent-runtime-name`, and must bind to the intended endpoint with `orka.ai/agent-runtime-endpoint`, preventing AgentRuntime authors from using the controller as a generic Secret exfiltration path. -- Built-in CLI runtime behavior remains backward compatible. -- External runtimes can prove the cross-repo contract without placing provider-specific implementations in the Orka repository or making Orka own Deployment, ServiceAccount, NetworkPolicy, image policy, or secret-delivery hardening. -- Brokered Orka Tool execution is opt-in by capability and policy; observed mode must not be marketed as full brokered governance. - -## External adapter references - -- Microsoft Foundry Hosted Agents (Responses API): [`orka-agents/agent-runtime-foundry`](https://github.com/orka-agents/agent-runtime-foundry) -- Microsoft Foundry Agent Service classic (Threads/Runs API): [`orka-agents/agent-runtime-foundry-classic`](https://github.com/orka-agents/agent-runtime-foundry-classic) - -## References - -- `docs/development/harness-protocol-mvp.md` -- `docs/development/harness-conformance.md` -- `internal/harness/` -- Planned issue direction: bring-your-own agent runtimes as governed `AgentRuntime` entries rather than free-form per-Agent images. diff --git a/docs/adr/0009-defer-runtime-session-ui-until-public-api.md b/docs/adr/0009-defer-runtime-session-ui-until-public-api.md index 21562a7f4..955748e96 100644 --- a/docs/adr/0009-defer-runtime-session-ui-until-public-api.md +++ b/docs/adr/0009-defer-runtime-session-ui-until-public-api.md @@ -4,7 +4,7 @@ Date: 2026-06-13 ## Status -Accepted. +Accepted; updated for the ACP v2 cutover. ## Context @@ -13,13 +13,13 @@ fork) is shipping in the web UI. Its final phase asks whether the UI should also surface RuntimeSession lifecycle (claim, reuse, release, retain, suspend, delete). -Per ADR 0008, RuntimeSession persistence starts as an internal `internal/harness` -state model with **no public CRD or HTTP API**. A scan of the server routes -(`internal/api/server.go`) confirms there is currently no -`runtimesession`/`runtime-session` endpoint — `RuntimeSession*` types exist only -in `internal/harness` as the turn-protocol contract. The only session-scoped HTTP -surfaces are the conversation-session endpoints (`/api/v1/sessions/...`), which -are unrelated to runtime sessions. +The ACP hard cutover now stores RuntimeSession control, ownership, lifecycle, +and mutation-lease state in Kubernetes-authoritative +`RuntimeSessionControl` records and coordination Leases. The public HTTP surface +still exposes Task execution/delivery projections and read-only RuntimePool +endpoints rather than direct RuntimeSession mutation. Conversation session +endpoints (`/api/v1/sessions/...`) remain a separate canonical-transcript +surface backed by SQLite payload storage. The UI follow-up plan is explicit that the UI must not invent backend behavior or call endpoints that do not exist. @@ -31,13 +31,14 @@ public runtime-session API exists. No runtime-session API client, hook, route, o component ships in this follow-up. This ADR is the documented follow-up that the plan requires. -When a public API does land (reading from the internal store first, then a -CRD-backed implementation per ADR 0008), add a feature-gated runtime-session view -that surfaces, per session: +When a public read API does land, it must project the Kubernetes-authoritative +control record rather than create a second SQLite authority. Add a +feature-gated runtime-session view that surfaces, per session: - runtime session id - namespace -- provider +- RuntimePool and exact runtime instance identity +- provider/model profile - state / phase - active task (linkable to task detail) - idle age (and idle timeout) @@ -54,14 +55,13 @@ execution-event surfaces already use. - No UI depends on unimplemented runtime-session backend behavior; nothing calls a nonexistent endpoint. -- The field list above is fixed up front, matching the migration-preserving fields - in ADR 0008, so a future implementation has a clear target. +- The field list above is fixed up front and must remain a safe projection of + `RuntimeSessionControl`, RuntimePool identity, and non-secret transcript data. - When the API ships, this is additive UI work behind a capability check rather than a redesign. ## Revisit -Revisit when the non-Substrate provider passes conformance and a public -runtime-session read API (internal-store-backed or CRD-backed) is exposed under -`/api/v1`. At that point, implement the feature-gated view described above and -update the UI guide (`website/docs/guides/ui.md`). +Revisit when a public RuntimeSession read API is exposed under `/api/v1`. At +that point, implement the feature-gated view described above and update the UI +guide (`website/docs/guides/ui.md`). diff --git a/docs/development/acp-crd-hard-cutover.md b/docs/development/acp-crd-hard-cutover.md new file mode 100644 index 000000000..08bfa6f65 --- /dev/null +++ b/docs/development/acp-crd-hard-cutover.md @@ -0,0 +1,207 @@ +# ACP v2 CRD hard cutover + +`scripts/upgrade-orka-crds.sh` is the only supported helper for replacing a +cluster that may still contain `orka.harness.v1` AgentRuntime state. It is a +fail-closed gate, not a migration tool. It never migrates or deletes +AgentRuntime, Agent, or GatewayBinding objects. + +Do not store backups, verification markers, or copied SQLite data in the +repository. They may contain sensitive operational data. + +## 1. Create and verify both backups + +Create a consistent backup of the controller SQLite/PVC state. A CSI snapshot, +offline PVC archive, raw SQLite backup, or provider snapshot receipt is +acceptable as the `--sqlite-backup` file, but the underlying backup must be +restore-tested or independently inspected before its marker is written. A +snapshot receipt file must identify the immutable snapshot that was tested. + +Export the pre-cutover CR inventory as one Kubernetes JSON List before deleting +or migrating legacy objects: + +```bash +set -Eeuo pipefail +umask 077 + +context=sertac-aks +backup_dir=/absolute/secure/path/orka-acp-cutover +install -d -m 0700 "$backup_dir" +chmod 0700 "$backup_dir" + +cluster_uid="$( + kubectl --context "$context" get namespace kube-system \ + -o jsonpath='{.metadata.uid}' +)" +api_server_identity_sha256="$( + kubectl --context "$context" config view --minify --flatten -o json \ + | jq -c ' + .clusters + | if length == 1 then + .[0].cluster + | { + server: (.server // null), + certificateAuthorityData: (.["certificate-authority-data"] // null), + insecureSkipTLSVerify: (.["insecure-skip-tls-verify"] // null), + tlsServerName: (.["tls-server-name"] // null), + proxyURL: (.["proxy-url"] // null), + disableCompression: (.["disable-compression"] // null) + } + else + error("expected exactly one target cluster") + end + ' \ + | shasum -a 256 \ + | awk '{print tolower($1)}' +)" + +cr_tmp="$(mktemp "$backup_dir/.orka-crs.XXXXXX")" +cleanup_cr_tmp() { + rm -f "$cr_tmp" +} +trap cleanup_cr_tmp EXIT + +if ! kubectl --context "$context" get \ + agentruntimes.core.orka.ai,agents.core.orka.ai,gatewaybindings.gateway.orka.ai \ + --all-namespaces -o json >"$cr_tmp"; then + echo "CR inventory capture failed; refusing to create a backup" >&2 + exit 1 +fi +if ! jq -e '.kind == "List" and (.items | type == "array")' \ + "$cr_tmp" >/dev/null; then + echo "CR inventory validation failed; refusing to create a backup" >&2 + exit 1 +fi +chmod 0600 "$cr_tmp" +mv "$cr_tmp" "$backup_dir/orka-crs.json" +trap - EXIT +``` + +After verifying each backup, create a digest-bound operator attestation. Use +`kind=sqlite-pvc` for the SQLite/PVC artifact and `kind=orka-crs` for the CR +inventory: + +```bash +write_verified_marker() { + kind="$1" + backup="$2" + marker="$3" + digest="$(shasum -a 256 "$backup" | awk '{print tolower($1)}')" + verified_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + cat >"$marker" < Booting -> Ready -> TurnRunning -> Idle -> Releasing -> Deleted - | | +-> Retained - | | +-> Suspended - | +-> Deleting -> Deleted - +-> Failed/Unhealthy -> Deleting -``` - -Supported states are `Pending`, `Booting`, `Ready`, `TurnRunning`, `Idle`, `Releasing`, `Retained`, `Suspended`, `Deleting`, `Deleted`, `Failed`, and `Unhealthy`. Runtime sessions require namespace, session name, provider, cleanup policy, and owner metadata. Cleanup policies are `delete`, `retain`, and provider-capability-gated `suspend`. - -### Security requirements - -- Harness control calls are namespace-scoped and authenticated by Orka; per-turn credentials must be short-lived and scoped to the task/session. -- Raw secrets, raw TxTokens, environment dumps, cookies, API keys, and JWTs must not appear in **persisted or observable** surfaces: Task status, persisted annotations, execution events/frames, logs, or trace output. Resolved literal credentials MAY be carried in the in-memory `StartTurnRequest.input.env` solely as the controller-to-wrapper delivery channel (see "Required turn fields"); the wrapper must not log the request and should drop `input.env` from retained turn state once child env is materialized. Raw TxTokens are disallowed even on the delivery channel — use owner-referenced child Secrets and fail-closed TTS exchanges. Confidentiality of the delivery channel in transit (TLS/mTLS) is a deployment-posture concern. -- Cross-namespace runtime reuse is denied by ownership validation. -- Lifecycle transitions and cleanup failures must be evented with safe metadata only. - -### Conformance contract - -The reusable conformance suite in `internal/harness/harnesstest` verifies health, capabilities, successful turns, failed turns, cancellation, invalid/unknown frames, redaction, and client timeout behavior against any provider factory. The fake harness server covers success, failure, delayed output, long-running turns, cancellation, invalid frames, and secret-looking output. diff --git a/docs/development/post-p0-evented-runtime-checklist.md b/docs/development/post-p0-evented-runtime-checklist.md index f05291471..962b5f32d 100644 --- a/docs/development/post-p0-evented-runtime-checklist.md +++ b/docs/development/post-p0-evented-runtime-checklist.md @@ -16,7 +16,7 @@ This checklist maps the P0 wave plans in `~/Downloads/orka-p0-wave-plans/` and t | Public task SSE stream | Implemented + tested | Replay, live polling, heartbeat, terminal `stream_complete`, and reconnect tests. | | Controller lifecycle producers | Implemented + tested | Task controller emits lifecycle events; focused controller event tests exist, envtest full suite requires envtest binaries. | | AI worker producers | Implemented + tested | `workers/ai` event tests cover model/tool/result/context events and redaction. | -| Agent CLI producers | Implemented + tested | agent runtime event tests cover command/runtime lifecycle. | +| ACP runtime producers | Foundation implemented + tested | Dispatcher, RuntimePool, v2 supervisor, and delivery state tests cover the session-centric path; full live/restart/publication acceptance remains required. | | Container/general worker producer | Implemented + tested | worker/common and general worker event/result tests cover basic lifecycle. | | Redaction/truncation | Implemented + tested | Worker-side and store-side redaction tests cover bearer/JWT/API key/cookie/GitHub/Anthropic/OpenAI/Txn token patterns. | | No Agent Substrate dependency | Implemented | Event store, APIs, trace, session aggregation, fork, and approvals use normal Task/Job event streams. | @@ -47,7 +47,7 @@ This checklist maps the P0 wave plans in `~/Downloads/orka-p0-wave-plans/` and t | F4 | Integrate one high-risk action | Deferred integration | First target is PR creation/merge; API/read model is ready for worker/tool integration. | | F5 | CLI approvals | Implemented | `orka task approvals/approve/decline`. | | G1-G2 | Event metrics/SLO hooks | Implemented baseline | Metric names and safe labels are documented; append-time idempotent pair derivation can be expanded as producers stabilize. | -| H | Harness protocol prep | Documented | See `docs/development/harness-protocol-mvp.md`. | +| H | ACP runtime protocol | Superseded by v2 | The turn-oriented prototype was removed. The current contract is `orka.harness.v2`; see `website/docs/development/agent-runtime-adapter-contract.md`. | ## Release notes draft diff --git a/docs/development/remaining-frontier-readiness.md b/docs/development/remaining-frontier-readiness.md index ba12dadb6..e2b598b75 100644 --- a/docs/development/remaining-frontier-readiness.md +++ b/docs/development/remaining-frontier-readiness.md @@ -1,6 +1,6 @@ # Remaining frontier readiness checklist -Date: 2026-06-11 +Date: 2026-07-25 This checklist gates implementation of the backend-neutral resident runtime frontier on top of the post-P0 evented runtime worktree. @@ -11,24 +11,26 @@ This checklist gates implementation of the backend-neutral resident runtime fron | Fork/checkpoint MVP | Ready | Event-sequence fork API and CLI are present; physical snapshot fork remains deferred. | | Durable approvals MVP | Ready for API/read model | Approval event read model and decision endpoints exist; first high-risk tool integration is still deferred. | | Event metrics | Ready baseline | Append/list/stream/redaction metrics exist with low-cardinality labels. | -| Harness protocol DTOs | Ready | `internal/harness` defines `orka.harness.v1` DTOs and validation. | -| Event mapping | Ready for conformance | `internal/harness.MapFrameToExecutionEvent` maps frames to existing execution events. | -| Runtime lifecycle state machine | Ready foundation | `internal/harness` validates RuntimeSession states and transitions. | -| Tool execution modes | Ready contract | Observed and brokered modes plus brokered idempotency key are defined. | -| Security requirements | Ready contract | Protocol docs and mapper enforce no raw secrets in persisted events. | -| Conformance fixture | Ready foundation | `internal/harness/harnesstest` fake server and suite cover MVP provider behavior. | -| Non-Substrate provider | Not implemented | The first Kubernetes Service/sidecar provider should be built after the conformance suite is adopted by controller integration. | -| RuntimeSession persistence/API | Deferred | ADR 0008 selects internal-store-first; persistent implementation follows provider integration. | -| Resident daemon/process | Deferred | Requires runtime session claim/release and provider implementation. | -| Substrate provider | Deferred optional | Must remain provider-scoped and pass the same conformance suite. | +| ACP v2 DTOs | Ready foundation | `internal/harness/v2` defines the session-centric `orka.harness.v2` request, event, lifecycle, duplicate, capability, and fencing contracts. | +| Event mapping | Ready foundation | ACP v2 prompt events are bounded and mapped into Orka-owned execution/result state; runtime diagnostics are not canonical Task authority. | +| Runtime lifecycle state machine | Ready foundation | `internal/harness/v2` validates RuntimeSession states from `creating` through validation/publication/finalization and deletion. | +| Prompt-scoped broker authority | Implemented foundation | Each RuntimeSession exposes a credential-protected loopback MCP endpoint. The controller revalidates Task, attempt, prompt lease, exact runtime fences, tool policy, approval evidence, and consequential-effect identity for every call. | +| Security requirements | Implemented foundation | V2 fences, operation capabilities, bounded diagnostics, private session identities, the central provider proxy, clean-room Git credential separation, and artifact/credential brokers are modeled and tested. | +| Conformance fixture | Ready foundation | `internal/harness/v2/conformance` and `conformancetest` cover v2 identity, endpoints, duplicates, fencing, cancellation, and governance claims. | +| Kubernetes RuntimePool provider | Implemented foundation | Controller-owned RuntimePools, exact-Pod routing, atomic status reservations, bounded-wait queue promotion, drain, scale-to-zero, and Codex/Claude/Copilot ACP images exist; full live acceptance remains required. | +| Kubernetes control authority | Implemented foundation | `ControllerEpoch`, `PromptAttempt`, `RuntimeSessionControl`, `BranchClaim`, `Publication`, and `ExternalEffect` status plus coordination Leases are authoritative. SQLite retains transcript/SessionTurn, deferred outbox, and artifact payloads rather than ACP control authority. | +| RuntimeSession persistence/API | Implemented foundation | Kubernetes control records and Leases fence restart/takeover recovery; SQLite persists transcript/session-turn payloads behind those fences. Full live restart/takeover acceptance remains required. | +| External v2 runtime dispatch | Deferred | Registration, probing, and conformance are available, but `runtimeRef` Task planning remains fail-closed until the external v2 dispatcher support boundary is enabled. | +| Resident supervisor/process | Implemented foundation | The ACP supervisor hosts multiple private RuntimeSessions with bounded prompt concurrency and cleanup rules. | +| Substrate provider | Deferred | Must host one RuntimeSession per Actor behind the same v2 lifecycle and pass the same conformance, crash, credential, and publication gates. | | Snapshot-aware fork | Deferred | Logical fork remains available; physical clone/snapshot capability contract follows provider support. | -No missing required contract blocks Wave 1 conformance/client/fake-harness work. Wave 2+ implementation should start by wiring a non-Substrate provider to the frozen DTOs and conformance suite. +The v2 contract and Kubernetes RuntimePool foundation have replaced the earlier turn-oriented frontier. The remaining release gate is the required live acceptance matrix: digest verification, Codex then Claude execution, continuation, active-prompt cancellation/timeout, restart and replacement recovery, clean-room publication/PR reconciliation, drain/scale-to-zero, and cleanup. Focused verification for the readiness layer: ```bash -go test ./internal/harness/... -run 'DTO|Protocol|RuntimeSession|MapFrame|FakeHarness|Conformance' -v +go test ./internal/harness/v2/... ./internal/acp/... -run 'Protocol|RuntimeSession|Conformance|Fence|Duplicate|Cancel' -v go test ./internal/api -run 'Task.*Trace|Session.*Event|Fork|Approval|Event' -v go test ./internal/store/sqlite -run ExecutionEvent -v ``` diff --git a/docs/development/repository-security-scanning-implementation-plan.md b/docs/development/repository-security-scanning-implementation-plan.md index 1dae86c89..54bd516a8 100644 --- a/docs/development/repository-security-scanning-implementation-plan.md +++ b/docs/development/repository-security-scanning-implementation-plan.md @@ -10,7 +10,7 @@ This plan keeps Orka's existing architecture: - Scan, validation, and patch work continue to run as Kubernetes-backed `Task` resources. - Dynamic security data remains in SQLite. - Detailed outputs continue to flow through task artifacts. -- Credentials, transaction metadata, and authorization remain governed by existing Orka worker, Secret, RBAC, OIDC, and context-token controls. +- Credentials, transaction metadata, and authorization remain governed by Orka Secret roles, ACP operation fences, RBAC, OIDC, and context-token controls. ## Target Outcomes @@ -39,7 +39,7 @@ Orka already has: - SQLite-backed scan runs, threat models, findings, and patch proposals. - Artifact contracts for threat models, findings, validation output, and patch diffs. - Human-in-the-loop patch generation and PR creation. -- Worker isolation through non-root pods, read-only root filesystem, dropped capabilities, seccomp, optional runtime class, and writable `/tmp`, `/home/worker`, and `/workspace`. +- Native mapper/container isolation plus ACP RuntimePools with private RuntimeSessions, digest-pinned profiles, no Git credentials, and separate clean-room publication. Key gaps to close: diff --git a/docs/pr-monitor-clawsweeper-parity-plan.md b/docs/pr-monitor-clawsweeper-parity-plan.md index 471b4d89d..860215e3c 100644 --- a/docs/pr-monitor-clawsweeper-parity-plan.md +++ b/docs/pr-monitor-clawsweeper-parity-plan.md @@ -1,5 +1,10 @@ # PR Monitor to ClawSweeper Parity Plan +> Historical scope note: this plan describes repository-monitor application +> state. Its SQLite source-of-truth statements do not apply to ACP runtime +> control records. The ACP hard cutover makes control CRD status and Kubernetes +> Leases authoritative while retaining monitor history in SQLite. + This document describes how to evolve Orka's current PR monitor into an Orka-native maintainer automation system with capabilities comparable to ClawSweeper. @@ -618,8 +623,9 @@ Input: - branch write mode - constraints on files, tools, and allowed mutation -The repair task may use an agent runtime workspace. It should not directly -merge or close a PR. +The repair Task may use top-level `spec.workspace` with `intent: write`, separate +read/publication credentials, and an Orka-owned publication branch. The ACP child +must not push, merge, or close a PR directly. ### Repair Task Output diff --git a/examples/bring-your-own-agent-runtime-demo/README.md b/examples/bring-your-own-agent-runtime-demo/README.md deleted file mode 100644 index c8cc52b75..000000000 --- a/examples/bring-your-own-agent-runtime-demo/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# Bring-your-own AgentRuntime demo - -This package is the canonical entry point for the provider-neutral remote-runtime demo. It points at the runnable manifests used by the current repo-owned implementation and keeps the story independent of any one backend. - -```text -Orka API + governance plane - -> namespace-local AgentRuntime facade - -> remote execution backend adapter/runtime - -> Orka-brokered Tool CRDs -``` - -## What is runnable from this repository - -| Scenario | Path | Credentials | Brokered tools | -| --- | --- | --- | --- | -| Fibey observed + backend switch facades | `../fibey-custom-agent-demo` | Harness bearer token only for generic HTTP; adapter credentials for optional backends | Optional; default observed | -| Support escalation brokered read | `../support-escalation-runtime-demo` | Harness bearer token only | `support-ticket-lookup` read tool | - -AgentKit Serve adapter changes are intentionally not in this repository. The Foundry facade targets the separately maintained Microsoft Foundry Hosted Agents Responses adapter [`orka-agents/agent-runtime-foundry`](https://github.com/orka-agents/agent-runtime-foundry); Foundry and AgentKit samples remain namespace-local facades that point at operator-provided adapter Services. - -## Run the generic HTTP demo - -Build and load the reference generic HTTP harness fixture: - -```bash -docker build -t ghcr.io/orka-agents/orka/example-echo-harness:latest -f examples/harness/echo/Dockerfile . -kind load docker-image ghcr.io/orka-agents/orka/example-echo-harness:latest --name -``` - -Create the runtime bearer Secret out of band; do not commit real values. The Secret must contain a data key named `token`, must be labeled `orka.ai/agent-runtime-auth=true`, and must be annotated with the exact runtime endpoint: - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: support-http-runtime-token - labels: - orka.ai/agent-runtime-auth: "true" - orka.ai/agent-runtime-name: support-http-runtime - annotations: - orka.ai/agent-runtime-endpoint: http://support-http-runtime.default.svc.cluster.local:8080 -stringData: - token: -``` - -Apply and run: - -```bash -kubectl apply -k examples/support-escalation-runtime-demo -kubectl wait --for=condition=Ready agentruntime/support-http-runtime --timeout=60s -kubectl get task support-escalation-demo -o yaml -``` - -Expected flow: - -1. `AgentRuntime/support-http-runtime` probes `/v1/health`, `/v1/capabilities`, observed turn conformance, and the advertised brokered read profile. -2. `Task/support-escalation-demo` starts a remote turn through the namespace-local facade. -3. The runtime emits `ToolCallRequested` for `support-ticket-lookup`. -4. Orka validates `Task.spec.agentRuntime.allowedTools`, loads the same-namespace `Tool`, resolves any downstream credentials inside Orka, executes it, records brokered events, and returns a `ToolCallResult` via `/v1/turns/{turnID}/continue`. -5. The runtime completes and Orka stores the result. - -## Approval-gated write variant - -To exercise a write tool, add a `Tool` with `spec.brokeredToolClass: write`, include it in `Task.spec.agentRuntime.allowedTools`, and run the generic fixture with: - -```yaml -env: -- name: ORKA_REMOTE_HTTP_RUNTIME_BEHAVIOR - value: approval-tool -- name: ORKA_REMOTE_HTTP_RUNTIME_WRITE_TOOL_NAME - value: -``` - -The expected UX is unchanged across backends: - -```bash -orka task approvals -orka task approve -orka task result -``` - -Orka, not the remote runtime, creates the canonical `ApprovalRequested` event and executes the approved tool exactly once unless a prior execution ledger entry has an unknown outcome, in which case it fails closed instead of replaying a consequential side effect. - -## Backend switching - -Use `../fibey-custom-agent-demo/switch-backend.sh` to patch only `Task.spec.agentRef.name` between namespace-local facades: - -```bash -examples/fibey-custom-agent-demo/switch-backend.sh http -examples/fibey-custom-agent-demo/switch-backend.sh foundry -``` - -The workflow, Tool CRDs, approval UX, and task/result APIs remain Orka-owned. Remote backends receive safe tool schemas and scoped turn metadata only; they do not receive downstream Tool credentials. - -## Troubleshooting - -| Symptom | Likely cause | Fix | -| --- | --- | --- | -| `AgentRuntime` Ready=False mentioning `orka.ai/agent-runtime-auth` | bearer Secret missing opt-in label | add `orka.ai/agent-runtime-auth: "true"` | -| Ready=False endpoint binding error | Secret annotation does not match `spec.deployment.endpoint` | update `orka.ai/agent-runtime-endpoint` | -| Ready=False brokered class missing | runtime did not advertise a required `brokeredToolClasses` value | fix adapter capabilities or narrow `spec.capabilities` | -| Task fails `tool not allowed` | remote requested a tool not in `Task.spec.agentRuntime.allowedTools` | add the intended tool or reject the backend behavior | -| Task waits for approval | write tool requested and no human decision exists | use `orka task approvals` then approve/decline | -| Outcome unknown for write tool | controller saw a pre-execution ledger entry without terminal result | inspect downstream idempotency target; do not blindly replay | -| Result data truncated in parent summary | structured data exceeded `wait_for_tasks` bounds | store large payloads as artifacts and pass references | diff --git a/examples/bring-your-own-agent-runtime-demo/SECURITY.md b/examples/bring-your-own-agent-runtime-demo/SECURITY.md deleted file mode 100644 index e0dd52830..000000000 --- a/examples/bring-your-own-agent-runtime-demo/SECURITY.md +++ /dev/null @@ -1,29 +0,0 @@ -# Security model for bring-your-own AgentRuntime demos - -Remote execution backends are workload substrates, not governance authorities. - -> Remote agents may ask; Orka decides and executes. - -## Invariants - -- `AgentRuntime.spec.deployment.endpoint` must not contain credentials. -- Runtime bearer tokens live in Kubernetes Secrets and must opt in with `orka.ai/agent-runtime-auth=true`. -- Runtime auth Secrets are bound to the expected `AgentRuntime` name and endpoint. -- Remote runtimes receive safe tool schemas only: name, description, brokered class, and JSON parameters. -- Remote runtimes never receive Tool CRD execution URLs, auth Secret refs, headers, bearer tokens, kubeconfigs, or approval bypass credentials. -- Orka validates allowed tools/classes before every brokered call. -- Orka creates canonical approval events for write tools and verifies exact argument/spec digests before execution. -- Brokered write execution records a pre-execution ledger entry; unresolved prior executions fail closed instead of duplicating consequential side effects. - -## Demo-only controls - -The checked-in generic HTTP fixture uses bearer-token authentication and allows `http://` cluster-local service URLs for kind/local demos. Production adapters should run behind TLS or private networking and may add mTLS or signed short-lived turn credentials. - -## Do not commit - -- runtime bearer token values; -- Foundry credentials; -- AgentKit credentials; -- downstream tool API keys; -- raw transcripts or auth headers; -- kubeconfigs or service-account tokens. diff --git a/examples/fibey-custom-agent-demo/README.md b/examples/fibey-custom-agent-demo/README.md deleted file mode 100644 index 1e2b41a94..000000000 --- a/examples/fibey-custom-agent-demo/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Fibey bring-your-own agent runtime demo - -This demo exercises the first bring-your-own agent runtime slice: Orka registers a namespace-local `AgentRuntime` facade for a remote execution backend, then an `Agent` routes `type: agent` work to it with `spec.runtime.runtimeRef`. - -The checked-in backend is a deterministic generic HTTP harness fixture. It advertises `runtimeName: fibey-http-runtime`, supports `orka.harness.v1`, and runs in `observed` tool mode by default. AgentKit Serve and Foundry should plug in by swapping only the backend Service/adapter endpoint and `AgentRuntime` facade, not the Orka workflow. - -## Backend facades - -| Facade | Backend | Credentials | -| --- | --- | --- | -| `fibey-http-runtime` | Generic mock/self-hosted HTTP runtime | Harness bearer token only | -| `fibey-agentkit-runtime` | AgentKit Serve adapter | Adapter/runtime config only | -| `fibey-foundry-runtime` | [`orka-agents/agent-runtime-foundry`](https://github.com/orka-agents/agent-runtime-foundry) | Harness bearer Secret plus adapter Azure identity; no Orka Tool production credentials | - -`fibey-agentkit-runtime` is intentionally observed-only in the checked-in demo: it should show `toolExecutionModes: [observed]`, `supportsCancel: true`, and `supportsRuntimeSessions: true`, with no `brokeredToolClasses` or `supportsContinuation`. AgentKit brokered read/write/coordination exist only for deployments that explicitly enable those conformance-gated profiles. - -`fibey-foundry-runtime` is also observed-only by default. It targets a deployed Microsoft Foundry Hosted Agent through the dedicated Responses endpoint derived from `ORKA_FOUNDRY_PROJECT_ENDPOINT` and `ORKA_FOUNDRY_AGENT_NAME`. The adapter uses `DefaultAzureCredential`; Foundry access tokens and downstream Orka Tool credentials are never stored in the `AgentRuntime`. Brokered read/write are opt-in through `ORKA_FOUNDRY_BROKERED_TOOL_CLASSES` and must match the facade's advertised capabilities. - -All facades are namespace-local `AgentRuntime` objects. Remote execution backends do **not** receive production Orka Tool credentials. In brokered mode, remote backends request tools and Orka owns authorization, approvals, idempotency, credential resolution, execution/brokering, events, lineage, and audit. - -## Build/load the generic HTTP fixture image for kind - -```bash -docker build -t ghcr.io/orka-agents/orka/example-echo-harness:latest -f examples/harness/echo/Dockerfile . -kind load docker-image ghcr.io/orka-agents/orka/example-echo-harness:latest --name -``` - -The fixture can run scripted behaviors through `ORKA_REMOTE_HTTP_RUNTIME_BEHAVIOR`: - -- `success` — return a final result; -- `read-tool` — emit a brokered read-only tool request and observed result frame; -- `approval-tool` — emit an approval-pending frame and resume on `/v1/turns/{turnID}/continue`; -- `failure` — fail deterministically; -- `timeout` — emit a retryable timeout failure; -- `cancellation` — wait until the turn is cancelled. - -The default demo uses `success`/observed mode so it can run without external credentials or brokered-tool infrastructure. - -## Apply the demo - -```bash -kubectl apply -k examples/fibey-custom-agent-demo -kubectl wait --for=condition=Ready agentruntime/fibey-http-runtime --timeout=60s -kubectl get task fibey-quincy-north-alert -o yaml -``` - -Expected flow: - -1. `AgentRuntime/fibey-http-runtime` reads only a harness token Secret labeled `orka.ai/agent-runtime-auth: "true"`, scoped with `orka.ai/agent-runtime-name`, and endpoint-bound with `orka.ai/agent-runtime-endpoint` before probing `/v1/health` and `/v1/capabilities` and becoming Ready. -2. `Agent/fibey-remote-http` selects the runtime by `runtimeRef`. -3. `Task/fibey-quincy-north-alert` starts a harness turn against the generic HTTP runtime endpoint. -4. The task timeline shows `TurnStarted`, `RuntimeOutput`, and `TurnCompleted` frames mapped into native Orka execution events. - -A successful Task should include `status.harnessRuntime.runtimeRefName: fibey-http-runtime`, proving the resolved runtime target was frozen for the accepted turn. - -## Swapping backends - -To test AgentKit Serve or Foundry, keep the Orka workflow and tool policy the same. Replace only: - -- the backend Deployment/Service or external endpoint; -- the harness bearer-token Secret binding; -- the namespace-local `AgentRuntime` facade used by `Agent.spec.runtime.runtimeRef`. - -Optional facade manifests are checked in but not included in the default `kustomization.yaml` because they require separately deployed adapters: - -For a local/kind AgentKit observed-mode demo with no model credentials, build and -load an AgentKit test-agent image from the AgentKit Serve checkout, then deploy -the offline echo fixture Service used only for readiness/conformance demos. The -AgentKitfile used for this image must declare the Orka per-turn env names that the -controller sends, because AgentKit rejects undeclared `input.env` values. Add -these entries to the test AgentKitfile before building: - -```yaml -env: - - name: ORKA_CONTROLLER_URL - - name: ORKA_RESULT_ENDPOINT - - name: ORKA_PARENT_TASK - - name: ORKA_PRIOR_TASK - - name: ORKA_PRIOR_TASK_NAMESPACE - - name: ORKA_COORDINATION_DEPTH -``` - -Then build/load/apply: - -```bash -# From /path/to/agentkit.serve: -make build-agentkit build-serve build-test-agent AGENT_IMAGE=hello-agent:test -kind load docker-image hello-agent:test --name - -# From this Orka checkout: -kubectl apply -f examples/fibey-custom-agent-demo/secret-agentkit.yaml -kubectl apply -f examples/fibey-custom-agent-demo/agentkit-runtime-offline.example.yaml -kubectl apply -f examples/fibey-custom-agent-demo/agentruntime-agentkit.yaml -kubectl apply -f examples/fibey-custom-agent-demo/agent-agentkit.yaml -kubectl wait --for=condition=Ready agentruntime/fibey-agentkit-runtime --timeout=60s -``` - -The example deployment sets `AGENTKIT_PROTOCOL=orka`, reads -`AGENTKIT_AUTH_TOKEN` from the Orka client-auth Secret, and sets -`AGENTKIT_ORKA_OFFLINE_ECHO=1` so the AgentRuntime readiness probe and demo task -complete without live provider credentials. Remove `AGENTKIT_ORKA_OFFLINE_ECHO` -and provide normal model/runtime credentials for production AgentKit services. - -```bash -# AgentKit Serve observed-mode facade; requires a Service named fibey-agentkit-runtime. -kubectl apply -f examples/fibey-custom-agent-demo/secret-agentkit.yaml -kubectl apply -f examples/fibey-custom-agent-demo/agentruntime-agentkit.yaml -kubectl apply -f examples/fibey-custom-agent-demo/agent-agentkit.yaml -kubectl wait --for=condition=Ready agentruntime/fibey-agentkit-runtime --timeout=60s - -# Foundry Hosted Agents facade; requires a Service named fibey-foundry-runtime. -# Build/deploy github.com/orka-agents/agent-runtime-foundry with a Foundry project endpoint, -# Hosted Agent name, adapter bearer token, and an Azure SDK identity such as Workload Identity. -# The checked-in facade is observed-only. Enable ORKA_FOUNDRY_BROKERED_TOOL_CLASSES -# and update the facade only after the Hosted Agent passes matching conformance probes. -kubectl apply -f examples/fibey-custom-agent-demo/secret-foundry.yaml -kubectl apply -f examples/fibey-custom-agent-demo/agentruntime-foundry.yaml -kubectl apply -f examples/fibey-custom-agent-demo/agent-foundry.yaml -kubectl wait --for=condition=Ready agentruntime/fibey-foundry-runtime --timeout=60s -``` - -Run the same task against another backend by changing only `spec.agentRef.name`, for example: - -```bash -examples/fibey-custom-agent-demo/switch-backend.sh agentkit -examples/fibey-custom-agent-demo/switch-backend.sh foundry -examples/fibey-custom-agent-demo/switch-backend.sh http -``` - -The script validates the selected `AgentRuntime` and `Agent`, then patches only -the Task's `spec.agentRef.name`. - -Brokered mode is used only when the selected runtime advertises brokered capabilities and the task/agent exposes allowed tools. Current AgentKit Serve facades do not advertise brokered mode, so AgentKit-owned tools remain internal to AgentKit and Orka observes only lifecycle/output frames. Orka-owned side-effect tools stay behind Orka brokered governance; production tool credentials are not handed to the remote backend. diff --git a/examples/fibey-custom-agent-demo/agent-agentkit.yaml b/examples/fibey-custom-agent-demo/agent-agentkit.yaml deleted file mode 100644 index e15741a76..000000000 --- a/examples/fibey-custom-agent-demo/agent-agentkit.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Optional Agent that selects the AgentKit Serve backend facade. -apiVersion: core.orka.ai/v1alpha1 -kind: Agent -metadata: - name: fibey-remote-agentkit -spec: - runtime: - runtimeRef: - name: fibey-agentkit-runtime - systemPrompt: - inline: | - You are Fibey's AgentKit-backed incident scout. Produce a concise dossier and request Orka-brokered tools for evidence or consequential actions. diff --git a/examples/fibey-custom-agent-demo/agent-foundry.yaml b/examples/fibey-custom-agent-demo/agent-foundry.yaml deleted file mode 100644 index c921ab367..000000000 --- a/examples/fibey-custom-agent-demo/agent-foundry.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Optional Agent that selects the Microsoft Foundry Hosted Agents Responses facade. -apiVersion: core.orka.ai/v1alpha1 -kind: Agent -metadata: - name: fibey-remote-foundry -spec: - runtime: - runtimeRef: - name: fibey-foundry-runtime - systemPrompt: - inline: | - You are Fibey's Foundry Hosted Agent incident scout. Produce a concise dossier. Request Orka-brokered tools only when the selected runtime advertises them. diff --git a/examples/fibey-custom-agent-demo/agent.yaml b/examples/fibey-custom-agent-demo/agent.yaml deleted file mode 100644 index 4ce44ec4f..000000000 --- a/examples/fibey-custom-agent-demo/agent.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: Agent -metadata: - name: fibey-remote-http -spec: - runtime: - runtimeRef: - name: fibey-http-runtime - systemPrompt: - inline: | - You are Fibey's custom incident scout. Produce a concise dossier and propose any side-effecting work order instead of executing it directly. diff --git a/examples/fibey-custom-agent-demo/agentkit-runtime-offline.example.yaml b/examples/fibey-custom-agent-demo/agentkit-runtime-offline.example.yaml deleted file mode 100644 index 137908ada..000000000 --- a/examples/fibey-custom-agent-demo/agentkit-runtime-offline.example.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Optional local/kind AgentKit Serve deployment for the Fibey AgentKit facade. -# Build and load an AgentKit test-agent image named hello-agent:test, then apply -# this manifest with secret-agentkit.yaml, agentruntime-agentkit.yaml, and -# agent-agentkit.yaml. The offline echo fixture is for conformance/demo only; do -# not use AGENTKIT_ORKA_OFFLINE_ECHO in production AgentKit deployments. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: fibey-agentkit-runtime -spec: - replicas: 1 - selector: - matchLabels: - app: fibey-agentkit-runtime - template: - metadata: - labels: - app: fibey-agentkit-runtime - spec: - containers: - - name: agentkit - image: hello-agent:test - imagePullPolicy: IfNotPresent - env: - - name: AGENTKIT_PROTOCOL - value: orka - - name: AGENTKIT_BIND - value: 0.0.0.0 - - name: AGENTKIT_ORKA_OFFLINE_ECHO - value: "1" - - name: AGENTKIT_AUTH_TOKEN - valueFrom: - secretKeyRef: - name: fibey-agentkit-runtime-token - key: token - ports: - - name: http - containerPort: 8080 ---- -apiVersion: v1 -kind: Service -metadata: - name: fibey-agentkit-runtime -spec: - selector: - app: fibey-agentkit-runtime - ports: - - name: http - port: 8080 - targetPort: http diff --git a/examples/fibey-custom-agent-demo/agentruntime-agentkit.yaml b/examples/fibey-custom-agent-demo/agentruntime-agentkit.yaml deleted file mode 100644 index 8178fc79b..000000000 --- a/examples/fibey-custom-agent-demo/agentruntime-agentkit.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Optional namespace-local facade for an AgentKit Serve adapter implementing orka.harness.v1. -# Current AgentKit Serve Orka support is observed mode only; brokered tool classes -# stay unadvertised until AgentKit passes the corresponding conformance profiles. -# Apply this with secret-agentkit.yaml and point an Agent runtimeRef at fibey-agentkit-runtime -# after deploying the adapter Service named fibey-agentkit-runtime. -apiVersion: core.orka.ai/v1alpha1 -kind: AgentRuntime -metadata: - name: fibey-agentkit-runtime -spec: - contractVersion: orka.harness.v1 - deployment: - mode: external-endpoint - endpoint: http://fibey-agentkit-runtime.default.svc.cluster.local:8080 - clientAuth: - bearerTokenSecretRef: - name: fibey-agentkit-runtime-token - key: token - capabilities: - toolExecutionModes: - - observed - supportsCancel: true - supportsRuntimeSessions: true diff --git a/examples/fibey-custom-agent-demo/agentruntime-foundry.yaml b/examples/fibey-custom-agent-demo/agentruntime-foundry.yaml deleted file mode 100644 index 7cc76fe1f..000000000 --- a/examples/fibey-custom-agent-demo/agentruntime-foundry.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Optional namespace-local facade for github.com/orka-agents/agent-runtime-foundry. -# The checked-in facade matches the Hosted Agents adapter's observed-only default. -# Apply this with secret-foundry.yaml and point an Agent runtimeRef at fibey-foundry-runtime -# after deploying the adapter Service named fibey-foundry-runtime. -apiVersion: core.orka.ai/v1alpha1 -kind: AgentRuntime -metadata: - name: fibey-foundry-runtime -spec: - contractVersion: orka.harness.v1 - deployment: - mode: external-endpoint - endpoint: http://fibey-foundry-runtime.default.svc.cluster.local:8080 - clientAuth: - bearerTokenSecretRef: - name: fibey-foundry-runtime-token - key: token - capabilities: - toolExecutionModes: - - observed - supportsCancel: true - supportsRuntimeSessions: true diff --git a/examples/fibey-custom-agent-demo/agentruntime.yaml b/examples/fibey-custom-agent-demo/agentruntime.yaml deleted file mode 100644 index 1cd83083c..000000000 --- a/examples/fibey-custom-agent-demo/agentruntime.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: AgentRuntime -metadata: - name: fibey-http-runtime -spec: - contractVersion: orka.harness.v1 - deployment: - mode: external-endpoint - endpoint: http://fibey-http-runtime.default.svc.cluster.local:8080 - clientAuth: - bearerTokenSecretRef: - name: fibey-http-runtime-token - key: token - capabilities: - toolExecutionModes: - - observed - supportsCancel: true - supportsRuntimeSessions: true diff --git a/examples/fibey-custom-agent-demo/kustomization.yaml b/examples/fibey-custom-agent-demo/kustomization.yaml deleted file mode 100644 index 6bae51397..000000000 --- a/examples/fibey-custom-agent-demo/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -resources: -- secret.yaml -- mock-http-runtime-service.yaml -- agentruntime.yaml -- agent.yaml -- task.yaml diff --git a/examples/fibey-custom-agent-demo/mock-http-runtime-service.yaml b/examples/fibey-custom-agent-demo/mock-http-runtime-service.yaml deleted file mode 100644 index a8f166d43..000000000 --- a/examples/fibey-custom-agent-demo/mock-http-runtime-service.yaml +++ /dev/null @@ -1,50 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: fibey-http-runtime - labels: - app.kubernetes.io/name: fibey-http-runtime -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: fibey-http-runtime - template: - metadata: - labels: - app.kubernetes.io/name: fibey-http-runtime - spec: - containers: - - name: harness - image: ghcr.io/orka-agents/orka/example-echo-harness:latest - imagePullPolicy: IfNotPresent - env: - - name: ORKA_REMOTE_HTTP_RUNTIME_ADDR - value: :8080 - - name: ORKA_REMOTE_HTTP_RUNTIME_NAME - value: fibey-http-runtime - - name: ORKA_REMOTE_HTTP_RUNTIME_BEARER_TOKEN - valueFrom: - secretKeyRef: - name: fibey-http-runtime-token - key: token - ports: - - name: http - containerPort: 8080 - readinessProbe: - httpGet: - path: /v1/health - port: http - periodSeconds: 3 ---- -apiVersion: v1 -kind: Service -metadata: - name: fibey-http-runtime -spec: - selector: - app.kubernetes.io/name: fibey-http-runtime - ports: - - name: http - port: 8080 - targetPort: http diff --git a/examples/fibey-custom-agent-demo/secret-agentkit.yaml b/examples/fibey-custom-agent-demo/secret-agentkit.yaml deleted file mode 100644 index eb3e38386..000000000 --- a/examples/fibey-custom-agent-demo/secret-agentkit.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Optional harness bearer token for the AgentKit Serve adapter facade. -# This authenticates Orka to the adapter endpoint; it is not a downstream production tool credential. -apiVersion: v1 -kind: Secret -metadata: - name: fibey-agentkit-runtime-token - annotations: - orka.ai/agent-runtime-endpoint: http://fibey-agentkit-runtime.default.svc.cluster.local:8080 - labels: - orka.ai/agent-runtime-auth: "true" - orka.ai/agent-runtime-name: fibey-agentkit-runtime -stringData: - token: mock-token diff --git a/examples/fibey-custom-agent-demo/secret-foundry.yaml b/examples/fibey-custom-agent-demo/secret-foundry.yaml deleted file mode 100644 index 1664eb501..000000000 --- a/examples/fibey-custom-agent-demo/secret-foundry.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Optional harness bearer token for the Foundry adapter facade. -# This authenticates Orka to the adapter endpoint; Foundry authentication uses the adapter deployment's Azure identity. -apiVersion: v1 -kind: Secret -metadata: - name: fibey-foundry-runtime-token - annotations: - orka.ai/agent-runtime-endpoint: http://fibey-foundry-runtime.default.svc.cluster.local:8080 - labels: - orka.ai/agent-runtime-auth: "true" - orka.ai/agent-runtime-name: fibey-foundry-runtime -stringData: - token: mock-token diff --git a/examples/fibey-custom-agent-demo/secret.yaml b/examples/fibey-custom-agent-demo/secret.yaml deleted file mode 100644 index 4d235db17..000000000 --- a/examples/fibey-custom-agent-demo/secret.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: fibey-http-runtime-token - annotations: - orka.ai/agent-runtime-endpoint: http://fibey-http-runtime.default.svc.cluster.local:8080 - labels: - orka.ai/agent-runtime-auth: "true" - orka.ai/agent-runtime-name: fibey-http-runtime -stringData: - token: mock-token diff --git a/examples/fibey-custom-agent-demo/switch-backend.sh b/examples/fibey-custom-agent-demo/switch-backend.sh deleted file mode 100755 index fcfb41345..000000000 --- a/examples/fibey-custom-agent-demo/switch-backend.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat >&2 <<'USAGE' -Usage: switch-backend.sh [task-name] [namespace] - -Patches the Fibey demo Task to use the Agent bound to the selected namespace-local -AgentRuntime facade. The selected AgentRuntime/Agent manifests must already be -applied. This changes only the Orka Agent reference; workflow input and Tool -policy remain Orka-owned. -USAGE -} - -backend="${1:-}" -task_name="${2:-fibey-quincy-north-alert}" -namespace="${3:-default}" - -case "${backend}" in - http) - agent="fibey-remote-http" - runtime="fibey-http-runtime" - ;; - agentkit) - agent="fibey-remote-agentkit" - runtime="fibey-agentkit-runtime" - ;; - foundry) - agent="fibey-remote-foundry" - runtime="fibey-foundry-runtime" - ;; - -h|--help|help|"") - usage - exit 0 - ;; - *) - echo "unknown backend: ${backend}" >&2 - usage - exit 2 - ;; -esac - -kubectl -n "${namespace}" get agentruntime "${runtime}" >/dev/null -kubectl -n "${namespace}" get agent "${agent}" >/dev/null -kubectl -n "${namespace}" patch task "${task_name}" --type merge \ - -p "{\"spec\":{\"agentRef\":{\"name\":\"${agent}\"}}}" - -echo "${task_name} now targets ${agent} (${runtime}) in namespace ${namespace}" diff --git a/examples/fibey-custom-agent-demo/task.yaml b/examples/fibey-custom-agent-demo/task.yaml deleted file mode 100644 index 787f755d8..000000000 --- a/examples/fibey-custom-agent-demo/task.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: Task -metadata: - name: fibey-quincy-north-alert -spec: - type: agent - agentRef: - name: fibey-remote-http - prompt: | - Quincy North alert: pump telemetry is anomalous after overnight maintenance. - Investigate likely cause, summarize evidence, and propose the safest next action. diff --git a/examples/github-cicd/README.md b/examples/github-cicd/README.md index 3118638cc..aeb2ce0c5 100644 --- a/examples/github-cicd/README.md +++ b/examples/github-cicd/README.md @@ -1,61 +1,66 @@ # GitHub CI/CD Integration -This maintained example shows how to use Orka's multi-agent coordination to automate a GitHub PR workflow: +This example shows how to use Orka's multi-agent coordination with the ACP v2 workspace boundary: -1. An AI coordinator delegates code changes to a Claude Code runtime agent. -2. The runtime agent pushes a feature branch. -3. The coordinator opens a PR with Orka's built-in `create_pull_request` tool. -4. The coordinator waits for CI and merges with `auto_merge_pull_request`. -5. If CI fails, the coordinator loops back with fix feedback on the same branch. +1. An AI coordinator delegates a write-intent Task to a Claude ACP runtime. +2. The runtime edits the verified workspace but never receives Git credentials or publishes directly. +3. Orka's separate Workspace/Publisher prepares and verifies the branch update. +4. The coordinator opens a PR with `create_pull_request`, waits for CI, and merges with `auto_merge_pull_request`. +5. If CI fails, the coordinator delegates a focused repair against the same claimed branch. -## How It Works +## Credential roles -1. A **coordinator agent** (AI type with coordination enabled) receives a task -2. It delegates work to a **Claude Code agent** via `delegate_task`, including workspace details for clone, push, and PR creation -3. It uses built-in GitHub coordination tools to create the PR and auto-merge when checks pass -4. On CI failure, it can delegate a follow-up fix using `prior_task` +The example uses three independent credential roles: + +- `claude-credentials` — provider/proxy access for the Claude ACP runtime; +- `repository-read` — clone/read credential used only by the clean-room workspace boundary; +- `repository-publish` — branch/forge credential used only by the Workspace/Publisher and GitHub coordination tools. + +Neither Git Secret is delivered to the ACP process tree. ## Files | File | Description | -|------|-------------| -| `agents.yaml` | Coordinator and Claude Code agent definitions | -| `secret.yaml` | Example `git-credentials` Secret for clone/push/PR auth | -| `task.yaml` | Sample task to trigger the workflow | -| `github-actions-webhook.yaml` | Optional GitHub Actions workflow that triggers a branch-fix agent task on CI failure | +| --- | --- | +| `agents.yaml` | Coordinator and Claude Agent definitions with ACP-safe prompts | +| `secret.yaml` | Example read and publication credential Secrets | +| `task.yaml` | Sample coordinator Task | +| `github-actions-webhook.yaml` | Optional workflow that creates a direct ACP write Task after CI failure | ## Setup ```bash -# Update `spec.providerRef.name` in agents.yaml to match your Provider CRD. -# Edit secret.yaml and replace the token value. +# Update spec.providerRef.name in agents.yaml to match your Provider CRD. +# Replace the placeholder values in secret.yaml before applying it. kubectl apply -f examples/github-cicd/secret.yaml -# Create Claude runtime credentials if you do not already have them +# Create the Claude provider/proxy credential separately. kubectl create secret generic claude-credentials \ --from-literal=ANTHROPIC_API_KEY=sk-ant-your-key -# Deploy the example kubectl apply -k examples/github-cicd - -# Submit a task kubectl apply -f examples/github-cicd/task.yaml ``` -Before running the task, edit `task.yaml` and replace the placeholder repository details: +Before running the Task, edit `task.yaml` and replace: -- `gitRepo` -- `branch` -- `gitSecretRef` -- `pushBranch` +- `gitRepo` and `publicationGitRepo`; +- `branch` and `pushBranch`; +- `readCredentialRef` and `publicationCredentialRef`. -## Optional GitHub Actions Integration +A branch update is successful only when the child Task has a terminal verified `status.delivery` receipt. The ACP child reporting that it changed files is not proof of publication. -The `github-actions-webhook.yaml` file is a workflow you can copy into `.github/workflows/` in a repository. When a CI job fails, it creates a direct `type: agent` task for `claude-coder` to investigate and push a fix to the same branch. +## Optional GitHub Actions integration + +Copy `github-actions-webhook.yaml` into `.github/workflows/` in a repository. On CI failure it creates a direct `type: agent` Task with top-level `workspace.intent: write`. The runtime edits the checkout; the Workspace/Publisher owns the exact-ref push. Configure these repository secrets: -- `ORKA_API_URL` -- `ORKA_TOKEN` +- `ORKA_API_URL`; +- `ORKA_TOKEN`. + +The Orka Task namespace must contain the `repository-read` and `repository-publish` Secrets referenced by the payload. -And make sure the Orka namespace already has a `git-credentials` Secret that matches the name used in the workflow payload. +:::caution Current write-path limitation +This worktree still fails non-empty workspace deltas closed until dispatcher-to-publisher delivery is fully wired. Treat this example as the ACP v2 manifest shape, and require a verified delivery receipt in live testing. +::: diff --git a/examples/github-cicd/agents.yaml b/examples/github-cicd/agents.yaml index 1fab09c16..2bca80e46 100644 --- a/examples/github-cicd/agents.yaml +++ b/examples/github-cicd/agents.yaml @@ -1,4 +1,5 @@ -# Claude Code agent that writes code and pushes a feature branch. +# Claude ACP agent. It edits the materialized workspace but never commits, +# pushes, or creates pull requests; the Workspace/Publisher owns delivery. apiVersion: core.orka.ai/v1alpha1 kind: Agent metadata: @@ -17,20 +18,16 @@ spec: - Grep systemPrompt: inline: | - You are a software developer. When given a task: - 1. Work in the repository and branch provided through workspace config. - 2. Implement the requested changes cleanly. - 3. Run relevant tests if they exist. - 4. Commit with a descriptive message. - 5. Push to the configured pushBranch. - 6. Report what changed and any test results. - - Do not create or merge pull requests yourself. - The coordinator handles GitHub workflow steps with Orka's built-in tools. + You are a software developer. Work only in the workspace supplied by Orka. + Implement the requested changes and run focused tests when available. + Do not commit, change Git remotes/configuration, push, or create a pull request. + Orka validates the final tree and publishes it through a separate clean-room service. + Report changed files and test results. secretRef: name: claude-credentials --- -# Coordinator agent that orchestrates the full CI/CD flow with built-in PR tools. +# Coordinator agent that delegates implementation and uses Orka's GitHub tools +# only after the child Task has a verified branch-delivery receipt. apiVersion: core.orka.ai/v1alpha1 kind: Agent metadata: @@ -42,26 +39,25 @@ spec: name: claude-sonnet-4-20250514 systemPrompt: inline: | - You are a CI/CD coordinator. You manage the full lifecycle of code changes: + You are a CI/CD coordinator. - 1. Read the repository details from the task prompt: - - gitRepo - - branch - - gitSecretRef - - pushBranch - 2. Delegate the implementation to the claude-coder agent with that workspace config. - 3. Wait for the coder to finish. - 4. Create a pull request with create_pull_request. - Remember the PR number after the first PR is opened. + 1. Read these values from the task prompt: + - gitRepo and branch + - readCredentialRef + - publicationGitRepo and publicationCredentialRef + - pushBranch and prBaseBranch + 2. Delegate implementation to claude-coder with workspace.intent = write and + every workspace field above. The coder edits files only; Orka publishes. + 3. Wait for the child Task. Continue only when its delivery status proves the + branch was independently verified. + 4. Create the pull request with create_pull_request, using the child task name, + pushBranch, and prBaseBranch. 5. Call auto_merge_pull_request to wait for CI and merge automatically. - 6. If auto_merge_pull_request reports ci_failed or timeout: - - Summarize the failure for the coder. - - Delegate a fix to claude-coder using prior_task and the same pushBranch. - - Wait for the fix to finish. - - Reuse the existing PR and call auto_merge_pull_request again. - 7. Report the final PR URL and merge outcome. + 6. If CI fails or times out, summarize the failure and delegate a focused repair + against the same claimed publication branch, then wait and verify delivery again. + 7. Report the final PR URL, verified branch SHA, and merge outcome. - Use Orka's built-in PR tools rather than shelling out to GitHub CLI. + Never ask the ACP runtime to commit, push, run gh, or create the PR itself. coordination: enabled: true maxConcurrentChildren: 3 diff --git a/examples/github-cicd/github-actions-webhook.yaml b/examples/github-cicd/github-actions-webhook.yaml index 7124f222c..090bac798 100644 --- a/examples/github-cicd/github-actions-webhook.yaml +++ b/examples/github-cicd/github-actions-webhook.yaml @@ -1,11 +1,12 @@ # .github/workflows/ci-feedback.yml -# Copy this to your repository to trigger Orka auto-fix on CI failure +# Copy this to your repository to trigger an Orka ACP repair Task on CI failure. # -# Required secrets: -# ORKA_API_URL: URL of your Orka API (e.g., https://orka.example.com) -# ORKA_TOKEN: ServiceAccount token for Orka API auth -# Cluster prerequisite: -# - git-credentials Secret exists in the task namespace and contains a GitHub token +# Required repository secrets: +# ORKA_API_URL: URL of your Orka API (for example, https://orka.example.com) +# ORKA_TOKEN: ServiceAccount/OIDC token for Orka API authentication +# Cluster prerequisites: +# - repository-read Secret exists in the Task namespace +# - repository-publish Secret exists in the Task namespace name: CI Feedback to Orka on: workflow_run: @@ -17,32 +18,32 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'failure' }} runs-on: ubuntu-latest steps: - - name: Get failure logs + - name: Get failure context id: logs run: | - echo "run_id=${{ github.event.workflow_run.id }}" >> $GITHUB_OUTPUT - echo "branch=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT - echo "sha=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_OUTPUT - echo "repo=${{ github.repository }}" >> $GITHUB_OUTPUT + echo "branch=${{ github.event.workflow_run.head_branch }}" >> "$GITHUB_OUTPUT" + echo "sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT" + echo "repo=${{ github.repository }}" >> "$GITHUB_OUTPUT" - - name: Trigger Orka fix agent + - name: Trigger Orka repair agent run: | - curl -s -X POST "${{ secrets.ORKA_API_URL }}/api/v1/tasks" \ + curl -sS -X POST "${{ secrets.ORKA_API_URL }}/api/v1/tasks" \ -H "Authorization: Bearer ${{ secrets.ORKA_TOKEN }}" \ -H "Content-Type: application/json" \ -d '{ "type": "agent", "agentRef": {"name": "claude-coder"}, - "prompt": "CI failed on branch ${{ steps.logs.outputs.branch }} (commit ${{ steps.logs.outputs.sha }}). Check the CI failures, fix the code, and push to the same branch.", - "agentRuntime": { - "workspace": { - "gitRepo": "https://github.com/${{ steps.logs.outputs.repo }}.git", - "branch": "${{ steps.logs.outputs.branch }}", - "gitSecretRef": { - "name": "git-credentials" - } - } + "prompt": "CI failed on branch ${{ steps.logs.outputs.branch }} at ${{ steps.logs.outputs.sha }}. Fix only the CI failure. Do not commit or push; Orka owns publication.", + "workspace": { + "intent": "write", + "gitRepo": "https://github.com/${{ steps.logs.outputs.repo }}.git", + "branch": "${{ steps.logs.outputs.branch }}", + "readCredentialRef": {"name": "repository-read"}, + "publicationGitRepo": "https://github.com/${{ steps.logs.outputs.repo }}.git", + "publicationCredentialRef": {"name": "repository-publish"}, + "pushBranch": "${{ steps.logs.outputs.branch }}" }, - "timeout": "15m", + "agentRuntime": {"maxTurns": 40}, + "timeout": "20m", "priority": 900 }' diff --git a/examples/github-cicd/secret.yaml b/examples/github-cicd/secret.yaml index bad4f2a0b..643c42b6b 100644 --- a/examples/github-cicd/secret.yaml +++ b/examples/github-cicd/secret.yaml @@ -1,7 +1,18 @@ apiVersion: v1 kind: Secret metadata: - name: git-credentials + name: repository-read type: Opaque stringData: - token: "ghp_YOUR_TOKEN_HERE" # needs repo, pull_request, and workflow scopes + # Use a read-only, repository-scoped token. + token: "github_pat_READ_ONLY_PLACEHOLDER" +--- +apiVersion: v1 +kind: Secret +metadata: + name: repository-publish +type: Opaque +stringData: + # Use a separately scoped token permitted to update the publication branch + # and create/reconcile pull requests. + token: "github_pat_PUBLICATION_PLACEHOLDER" diff --git a/examples/github-cicd/task.yaml b/examples/github-cicd/task.yaml index dc18e2e68..d43faa313 100644 --- a/examples/github-cicd/task.yaml +++ b/examples/github-cicd/task.yaml @@ -10,15 +10,19 @@ spec: prompt: | Implement unit tests for the authentication module. - Repository details: + Repository details for the delegated ACP write Task: - gitRepo: https://github.com/myorg/myrepo.git - branch: main - - gitSecretRef: git-credentials + - readCredentialRef: repository-read + - publicationGitRepo: https://github.com/myorg/myrepo.git + - publicationCredentialRef: repository-publish - pushBranch: chore/add-auth-tests + - prBaseBranch: main Requirements: - Cover the authentication module's happy path and failure path. - Run the relevant test suite before handing work back. + - Require a verified branch-delivery receipt before opening the PR. - Open a PR and auto-merge it once CI passes. timeout: 45m priority: 800 diff --git a/examples/github-label-trigger/README.md b/examples/github-label-trigger/README.md index 465f39bea..3d9b9aaea 100644 --- a/examples/github-label-trigger/README.md +++ b/examples/github-label-trigger/README.md @@ -1,9 +1,9 @@ # GitHub Label Trigger Example -This example wires GitHub labels to Orka runtime-agent Tasks. +This example wires GitHub labels to Orka ACP `type: agent` Tasks. 1. Deploy an Agent that can work in a git workspace. -2. Configure the controller with a webhook secret, default Agent, and git credentials Secret. +2. Configure the controller with a webhook secret, default Agent, and the label-trigger compatibility Git Secret. 3. Add labels such as `agent:implement`, `agent:update-branch`, `agent:review`, or `agent:to-issues` to issues/PRs. ## Secrets @@ -15,10 +15,10 @@ kubectl create secret generic github-webhook-secret \ --from-literal=secret='' kubectl create secret generic git-credentials \ - --from-literal=token='' + --from-literal=token='' ``` -The GitHub token should have only the repository permissions required by the actions you allow. Runtime agents should leave workspace changes uncommitted; Orka finalization commits and pushes configured branches. +The compatibility setting maps this Secret to the Task read role and, for write labels, the publication role. The ACP runtime never receives it. Runtime agents must leave changes uncommitted; the separate Workspace/Publisher prepares, publishes, and verifies configured branches. For strict least privilege with different read and publication Secrets, create Tasks through an API/workflow that sets both top-level references explicitly. ## Controller env diff --git a/examples/harness/echo/Dockerfile b/examples/harness/echo/Dockerfile deleted file mode 100644 index ae5b0354e..000000000 --- a/examples/harness/echo/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM golang:1.26-alpine AS build -WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o /out/orka-example-echo-harness ./examples/harness/echo - -FROM gcr.io/distroless/static:nonroot -COPY --from=build /out/orka-example-echo-harness /orka-example-echo-harness -USER 65532:65532 -EXPOSE 8090 -ENTRYPOINT ["/orka-example-echo-harness"] diff --git a/examples/harness/echo/main.go b/examples/harness/echo/main.go deleted file mode 100644 index 5bdc12572..000000000 --- a/examples/harness/echo/main.go +++ /dev/null @@ -1,638 +0,0 @@ -package main - -import ( - "crypto/subtle" - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "strings" - "sync" - "time" - - "github.com/orka-agents/orka/internal/harness" -) - -const ( - behaviorSuccess = "success" - behaviorReadTool = "read-tool" - behaviorApprovalTool = "approval-tool" - behaviorFailure = "failure" - behaviorTimeout = "timeout" - behaviorCancellation = "cancellation" - remoteRuntimeNameEnv = "ORKA_REMOTE_HTTP_RUNTIME_NAME" - remoteRuntimeBearerEnv = "ORKA_REMOTE_HTTP_RUNTIME_BEARER_TOKEN" - remoteRuntimeAddrEnv = "ORKA_REMOTE_HTTP_RUNTIME_ADDR" - remoteRuntimeScriptEnv = "ORKA_REMOTE_HTTP_RUNTIME_BEHAVIOR" - remoteRuntimeReadToolEnv = "ORKA_REMOTE_HTTP_RUNTIME_READ_TOOL_NAME" - remoteRuntimeWriteToolEnv = "ORKA_REMOTE_HTTP_RUNTIME_WRITE_TOOL_NAME" - remoteRuntimeBrokeredOnlyEnv = "ORKA_REMOTE_HTTP_RUNTIME_BROKERED_ONLY" - brokeredReadCallID = "tool-read-1" - brokeredWriteCallID = "tool-write-1" -) - -type server struct { - runtimeName string - bearerValue string - behavior string - mu sync.Mutex - turns map[harness.HarnessTurnID]*turnState - completedTurns map[harness.HarnessTurnID]struct{} -} - -type turnState struct { - request harness.StartTurnRequest - cancelled chan struct{} - continued chan struct{} - onceCancel sync.Once - onceCont sync.Once - results []harness.ToolCallResult -} - -func main() { - addr := firstNonBlank(os.Getenv(remoteRuntimeAddrEnv), os.Getenv("ORKA_EXAMPLE_HARNESS_ADDR"), ":8090") - runtimeName := firstNonBlank( - os.Getenv(remoteRuntimeNameEnv), - os.Getenv("ORKA_EXAMPLE_HARNESS_RUNTIME_NAME"), - "orka-generic-http-runtime", - ) - behavior := normalizeBehavior(firstNonBlank( - os.Getenv(remoteRuntimeScriptEnv), - os.Getenv("ORKA_EXAMPLE_HARNESS_BEHAVIOR"), - behaviorSuccess, - )) - s := &server{ - runtimeName: runtimeName, - bearerValue: strings.TrimSpace(firstNonBlank( - os.Getenv(remoteRuntimeBearerEnv), - os.Getenv("ORKA_EXAMPLE_HARNESS_BEARER_TOKEN"), - )), - behavior: behavior, - turns: map[harness.HarnessTurnID]*turnState{}, - completedTurns: map[harness.HarnessTurnID]struct{}{}, - } - mux := http.NewServeMux() - mux.HandleFunc(harness.HealthPath, s.health) - mux.HandleFunc(harness.CapabilitiesPath, s.capabilities) - mux.HandleFunc(harness.TurnsPath, s.startTurn) - mux.HandleFunc(harness.TurnsPath+"/", s.turn) - mux.HandleFunc("/lookup", s.supportLookup) - log.Printf("generic HTTP AgentRuntime fixture listening on %s (runtime=%s behavior=%s)", addr, runtimeName, behavior) - log.Fatal(http.ListenAndServe(addr, mux)) -} - -func (s *server) supportLookup(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - harness.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - var body map[string]any - _ = json.NewDecoder(r.Body).Decode(&body) - incident, _ := body["incident"].(string) - if strings.TrimSpace(incident) == "" { - incident = "unknown" - } - harness.WriteJSON(w, http.StatusOK, map[string]any{ - "success": true, - "data": map[string]any{ - "incident": incident, - "status": "investigating", - "source": "mock-support-tool", - }, - }) -} - -func firstNonBlank(values ...string) string { - for _, value := range values { - if strings.TrimSpace(value) != "" { - return strings.TrimSpace(value) - } - } - return "" -} - -func envBool(name string) bool { - switch strings.ToLower(strings.TrimSpace(os.Getenv(name))) { - case "1", "true", "yes", "on": - return true - default: - return false - } -} - -func normalizeBehavior(value string) string { - switch strings.TrimSpace(value) { - case behaviorReadTool, behaviorApprovalTool, behaviorFailure, behaviorTimeout, behaviorCancellation: - return strings.TrimSpace(value) - default: - return behaviorSuccess - } -} - -func (s *server) health(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - harness.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - harness.WriteJSON(w, http.StatusOK, harness.HealthResponse{ - Version: harness.ProtocolVersion, - Status: harness.HealthStatusOK, - Ready: true, - CheckedAt: time.Now().UTC(), - Metadata: map[string]string{ - "runtime": s.runtimeName, - "backend": "generic-http", - }, - }) -} - -func (s *server) capabilities(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - harness.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - modes := []harness.ToolExecutionMode{harness.ToolExecutionModeObserved} - classes := []harness.BrokeredToolClass(nil) - brokeredOnly := envBool(remoteRuntimeBrokeredOnlyEnv) - if brokeredOnly { - modes = nil - } - if s.behavior == behaviorReadTool { - modes = append(modes, harness.ToolExecutionModeBrokered) - classes = append(classes, harness.BrokeredToolClassRead) - } - if s.behavior == behaviorApprovalTool { - modes = append(modes, harness.ToolExecutionModeBrokered) - classes = append(classes, harness.BrokeredToolClassWrite) - } - harness.WriteJSON(w, http.StatusOK, harness.CapabilitiesResponse{ - Version: harness.ProtocolVersion, - ProtocolVersion: harness.ProtocolVersion, - Transport: harness.HTTPTransport, - RuntimeName: s.runtimeName, - RuntimeVersion: "generic-http-fixture", - ProviderKind: harness.ProviderKindRemote, - ToolExecutionModes: modes, - BrokeredToolClasses: classes, - SupportsCancel: true, - SupportsRuntimeSessions: true, - SupportsContinuation: s.behavior == behaviorReadTool || s.behavior == behaviorApprovalTool, - SupportsArtifacts: true, - MaxConcurrentTurns: 1, - MaxTurnSeconds: 600, - MaxOutputBytes: 1 << 20, - Metadata: map[string]string{ - "backend": "generic-http", - "behavior": s.behavior, - }, - }) -} - -func (s *server) authorized(w http.ResponseWriter, r *http.Request) bool { - if strings.TrimSpace(s.bearerValue) == "" { - return true - } - got := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")) - if got == "" || subtle.ConstantTimeCompare([]byte(got), []byte(s.bearerValue)) != 1 { - harness.WriteError(w, http.StatusUnauthorized, "unauthorized") - return false - } - return true -} - -func (s *server) startTurn(w http.ResponseWriter, r *http.Request) { - if !s.authorized(w, r) { - return - } - if r.Method != http.MethodPost { - harness.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - var request harness.StartTurnRequest - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - harness.WriteError(w, http.StatusBadRequest, "invalid JSON request") - return - } - if err := request.Validate(); err != nil { - harness.WriteError(w, http.StatusBadRequest, err.Error()) - return - } - eventStreamPath, err := harness.EventStreamPath(request.TurnID) - if err != nil { - harness.WriteError(w, http.StatusBadRequest, err.Error()) - return - } - turn := &turnState{request: request, cancelled: make(chan struct{}), continued: make(chan struct{})} - s.mu.Lock() - if _, completed := s.completedTurns[request.TurnID]; completed { - s.mu.Unlock() - harness.WriteError(w, http.StatusConflict, "turn already completed") - return - } - if _, exists := s.turns[request.TurnID]; exists { - s.mu.Unlock() - harness.WriteError(w, http.StatusConflict, "turn already exists") - return - } - s.turns[request.TurnID] = turn - s.mu.Unlock() - harness.WriteJSON(w, http.StatusAccepted, harness.StartTurnResponse{ - Version: harness.ProtocolVersion, - Accepted: true, - RuntimeSessionID: request.RuntimeSessionID, - TurnID: request.TurnID, - CorrelationID: request.CorrelationID, - EventStreamPath: eventStreamPath, - }) -} - -func (s *server) turn(w http.ResponseWriter, r *http.Request) { - if !s.authorized(w, r) { - return - } - turnID, resource, err := harness.ParseTurnResourcePath(r.URL.EscapedPath()) - if err != nil { - harness.WriteError(w, http.StatusNotFound, "not found") - return - } - s.mu.Lock() - turn := s.turns[turnID] - s.mu.Unlock() - if turn == nil { - harness.WriteError(w, http.StatusNotFound, "turn not found") - return - } - switch resource { - case harness.TurnResourceEvents: - if r.Method != http.MethodGet { - harness.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - s.streamEvents(w, r, turn) - case harness.TurnResourceContinue: - if r.Method != http.MethodPost { - harness.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - s.continueTurn(w, r, turn) - case harness.TurnResourceCancel: - s.cancelTurn(w, r, turn) - default: - harness.WriteError(w, http.StatusNotFound, "not found") - } -} - -func (s *server) streamEvents(w http.ResponseWriter, r *http.Request, turn *turnState) { - afterSeq := int64(0) - if raw := strings.TrimSpace(r.URL.Query().Get("afterSeq")); raw != "" { - _, _ = fmt.Sscanf(raw, "%d", &afterSeq) - } - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - write := func(frame harness.HarnessEventFrame) bool { - if frame.Seq <= afterSeq { - return true - } - return harness.WriteSSEFrame(w, frame) == nil - } - frames := s.initialFrames(turn) - if framesHaveTerminal(frames) { - s.markCompleted(turn.request.TurnID) - } - for _, frame := range frames { - if !write(frame) { - return - } - } - if turn.request.ToolExecutionMode != harness.ToolExecutionModeBrokered && - (s.behavior == behaviorReadTool || s.behavior == behaviorApprovalTool) { - _ = harness.WriteSSEDone(w) - return - } - switch s.behavior { - case behaviorReadTool: - if afterSeq >= 5 { - _ = harness.WriteSSEDone(w) - return - } - select { - case <-turn.continued: - continuedFrames := s.continuedReadFrames(turn) - if framesHaveTerminal(continuedFrames) { - s.markCompleted(turn.request.TurnID) - } - for _, frame := range continuedFrames { - if !write(frame) { - return - } - } - _ = harness.WriteSSEDone(w) - case <-turn.cancelled: - s.markCompleted(turn.request.TurnID) - _ = write(frame(turn.request, 4, harness.FrameTurnCancelled, "turn cancelled", nil)) - _ = harness.WriteSSEDone(w) - case <-r.Context().Done(): - return - } - case behaviorApprovalTool: - if afterSeq >= 6 { - _ = harness.WriteSSEDone(w) - return - } - select { - case <-turn.continued: - continuedFrames := s.continuedFrames(turn) - if framesHaveTerminal(continuedFrames) { - s.markCompleted(turn.request.TurnID) - } - for _, frame := range continuedFrames { - if !write(frame) { - return - } - } - _ = harness.WriteSSEDone(w) - case <-turn.cancelled: - s.markCompleted(turn.request.TurnID) - _ = write(frame(turn.request, 5, harness.FrameTurnCancelled, "turn cancelled", nil)) - _ = harness.WriteSSEDone(w) - case <-r.Context().Done(): - return - } - case behaviorCancellation: - select { - case <-turn.cancelled: - s.markCompleted(turn.request.TurnID) - _ = write(frame(turn.request, 2, harness.FrameTurnCancelled, "turn cancelled", nil)) - _ = harness.WriteSSEDone(w) - case <-r.Context().Done(): - return - } - default: - _ = harness.WriteSSEDone(w) - } -} - -func (s *server) markCompleted(turnID harness.HarnessTurnID) { - s.mu.Lock() - s.completedTurns[turnID] = struct{}{} - s.mu.Unlock() -} - -func framesHaveTerminal(frames []harness.HarnessEventFrame) bool { - for _, frame := range frames { - switch frame.Type { - case harness.FrameTurnCompleted, harness.FrameTurnFailed, harness.FrameTurnCancelled: - return true - } - } - return false -} - -func (s *server) initialFrames(turn *turnState) []harness.HarnessEventFrame { - request := turn.request - start := frame(request, 1, harness.FrameTurnStarted, "turn started", nil) - switch s.behavior { - case behaviorFailure: - failed := frame(request, 2, harness.FrameTurnFailed, "turn failed", nil) - failed.Failed = &harness.TurnFailed{Reason: "simulated_failure", Message: "generic HTTP runtime simulated failure"} - failed.Error = &harness.ErrorInfo{Code: "simulated_failure", Message: "generic HTTP runtime simulated failure"} - return []harness.HarnessEventFrame{start, failed} - case behaviorTimeout: - failed := frame(request, 2, harness.FrameTurnFailed, "turn timeout", nil) - failed.Failed = &harness.TurnFailed{ - Reason: "timeout", - Message: "generic HTTP runtime simulated timeout", - Retryable: true, - } - failed.Error = &harness.ErrorInfo{Code: "timeout", Message: "generic HTTP runtime simulated timeout", Retryable: true} - return []harness.HarnessEventFrame{start, failed} - case behaviorCancellation: - return []harness.HarnessEventFrame{start} - case behaviorReadTool: - if request.ToolExecutionMode != harness.ToolExecutionModeBrokered { - return observedSuccessFrames(request, start) - } - output := runtimeOutput(request, 2, "generic HTTP runtime requesting read-only tool") - toolName := brokeredToolNameForRequest(request, defaultReadToolName()) - if !requestIncludesBrokeredToolSchema(request, toolName) { - failed := missingToolSchemaFrame(request, 3) - return []harness.HarnessEventFrame{start, output, failed} - } - tool := toolRequested(request, 3, toolName, brokeredReadCallID, `{"incident":"quincy-north"}`) - return []harness.HarnessEventFrame{start, output, tool} - case behaviorApprovalTool: - if request.ToolExecutionMode != harness.ToolExecutionModeBrokered { - return observedSuccessFrames(request, start) - } - output := runtimeOutput(request, 2, "generic HTTP runtime requesting approval-gated tool") - toolName := brokeredToolNameForRequest(request, defaultWriteToolName()) - if !requestIncludesBrokeredToolSchema(request, toolName) { - failed := missingToolSchemaFrame(request, 3) - return []harness.HarnessEventFrame{start, output, failed} - } - - tool := toolRequested( - request, - 3, - toolName, - brokeredWriteCallID, - `{"incident":"quincy-north","action":"dispatch technician"}`, - ) - waiting := frame(request, 4, harness.FrameRuntimeLog, "waiting for Orka brokered approval", nil) - waiting.Content = json.RawMessage(fmt.Sprintf(`{"status":"waiting_for_orka_approval","targetTool":%q}`, toolName)) - return []harness.HarnessEventFrame{start, output, tool, waiting} - default: - return observedSuccessFrames(request, start) - } -} - -func missingToolSchemaFrame(request harness.StartTurnRequest, seq int64) harness.HarnessEventFrame { - const message = "brokered tool schema was not supplied by Orka" - failed := frame(request, seq, harness.FrameTurnFailed, "brokered tool schema missing", nil) - failed.Failed = &harness.TurnFailed{Reason: "missing_tool_schema", Message: message} - failed.Error = &harness.ErrorInfo{Code: "missing_tool_schema", Message: message} - return failed -} - -func requestIncludesBrokeredToolSchema(request harness.StartTurnRequest, name string) bool { - name = strings.TrimSpace(name) - for _, definition := range request.Input.Tools { - if strings.TrimSpace(definition.Name) == name && definition.BrokeredClass != "" { - return true - } - } - return false -} - -func brokeredToolNameForRequest(request harness.StartTurnRequest, fallback string) string { - if class := strings.TrimSpace(request.Metadata["brokeredToolClass"]); class != "" { - return "conformance_" + class - } - return fallback -} - -func defaultReadToolName() string { - return firstNonBlank(os.Getenv(remoteRuntimeReadToolEnv), "read_incident") -} - -func defaultWriteToolName() string { - return firstNonBlank(os.Getenv(remoteRuntimeWriteToolEnv), "dispatch_work_order") -} - -func observedSuccessFrames( - request harness.StartTurnRequest, - start harness.HarnessEventFrame, -) []harness.HarnessEventFrame { - output := runtimeOutput(request, 2, "echo: "+request.Input.Prompt) - completed := frame(request, 3, harness.FrameTurnCompleted, "turn completed", &harness.TurnCompleted{ - Result: "ok", - FinalEventSeq: 3, - }) - return []harness.HarnessEventFrame{start, output, completed} -} - -func (s *server) continuedReadFrames(turn *turnState) []harness.HarnessEventFrame { - request := turn.request - result := frame(request, 4, harness.FrameToolResultReceived, "brokered read-only tool result received", nil) - result.ToolName = brokeredToolNameForRequest(request, defaultReadToolName()) - result.ToolCallID = brokeredReadCallID - if len(turn.results) > 0 { - if turn.results[0].Error != nil { - result.Error = turn.results[0].Error - encoded, _ := json.Marshal(map[string]any{"error": turn.results[0].Error}) - result.Content = encoded - } else { - result.Content = turn.results[0].Output - } - } - completed := frame(request, 5, harness.FrameTurnCompleted, "turn completed", &harness.TurnCompleted{ - Result: "read-only investigation complete", - FinalEventSeq: 5, - }) - return []harness.HarnessEventFrame{result, completed} -} - -func (s *server) continuedFrames(turn *turnState) []harness.HarnessEventFrame { - request := turn.request - result := frame(request, 5, harness.FrameToolResultReceived, "brokered approval-gated tool result received", nil) - result.ToolName = brokeredToolNameForRequest(request, defaultWriteToolName()) - result.ToolCallID = brokeredWriteCallID - if len(turn.results) > 0 { - if turn.results[0].Error != nil { - result.Error = turn.results[0].Error - encoded, _ := json.Marshal(map[string]any{"error": turn.results[0].Error}) - result.Content = encoded - } else { - result.Content = turn.results[0].Output - } - } else { - result.Error = &harness.ErrorInfo{Code: "missing_tool_result", Message: "continue request had no tool result"} - result.Content = json.RawMessage(`{"error":{"code":"missing_tool_result"}}`) - } - completed := frame(request, 6, harness.FrameTurnCompleted, "turn completed", &harness.TurnCompleted{ - Result: "approval-gated action completed", - FinalEventSeq: 6, - }) - return []harness.HarnessEventFrame{result, completed} -} - -func runtimeOutput(request harness.StartTurnRequest, seq int64, text string) harness.HarnessEventFrame { - output := frame(request, seq, harness.FrameRuntimeOutput, "runtime output", nil) - output.ContentText = text - output.Content = json.RawMessage(fmt.Sprintf(`{"message":%q}`, text)) - return output -} - -func toolRequested( - request harness.StartTurnRequest, - seq int64, - name string, - toolCallID string, - args string, -) harness.HarnessEventFrame { - tool := frame(request, seq, harness.FrameToolCallRequested, "brokered tool requested", nil) - tool.ToolName = name - tool.ToolCallID = toolCallID - tool.Content = json.RawMessage(args) - return tool -} - -func (s *server) continueTurn(w http.ResponseWriter, r *http.Request, turn *turnState) { - var request harness.ContinueTurnRequest - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - harness.WriteError(w, http.StatusBadRequest, "invalid JSON request") - return - } - if err := request.Validate(); err != nil { - harness.WriteError(w, http.StatusBadRequest, err.Error()) - return - } - if request.RuntimeSessionID != turn.request.RuntimeSessionID || - request.TurnID != turn.request.TurnID || - request.CorrelationID != turn.request.CorrelationID { - harness.WriteError(w, http.StatusBadRequest, "continue request does not match started turn") - return - } - turn.results = append([]harness.ToolCallResult(nil), request.ToolResults...) - turn.onceCont.Do(func() { close(turn.continued) }) - harness.WriteJSON(w, http.StatusAccepted, harness.ContinueTurnResponse{ - Version: harness.ProtocolVersion, - Accepted: true, - RuntimeSessionID: request.RuntimeSessionID, - TurnID: request.TurnID, - CorrelationID: request.CorrelationID, - Message: "continue accepted", - }) -} - -func (s *server) cancelTurn(w http.ResponseWriter, r *http.Request, turn *turnState) { - if r.Method != http.MethodPost { - harness.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") - return - } - var request harness.CancelTurnRequest - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - harness.WriteError(w, http.StatusBadRequest, "invalid JSON request") - return - } - if err := request.Validate(); err != nil { - harness.WriteError(w, http.StatusBadRequest, err.Error()) - return - } - if request.RuntimeSessionID != turn.request.RuntimeSessionID || request.TurnID != turn.request.TurnID { - harness.WriteError(w, http.StatusBadRequest, "cancel request does not match started turn") - return - } - turn.onceCancel.Do(func() { close(turn.cancelled) }) - harness.WriteJSON(w, http.StatusAccepted, harness.CancelTurnResponse{ - Version: harness.ProtocolVersion, - Accepted: true, - RuntimeSessionID: request.RuntimeSessionID, - TurnID: request.TurnID, - CorrelationID: request.CorrelationID, - Message: "cancel accepted", - }) -} - -func frame( - request harness.StartTurnRequest, - seq int64, - typ harness.FrameType, - summary string, - completed *harness.TurnCompleted, -) harness.HarnessEventFrame { - metadata := map[string]string{"backend": "generic-http"} - return harness.HarnessEventFrame{ - Version: harness.ProtocolVersion, - Type: typ, - RuntimeSessionID: request.RuntimeSessionID, - TurnID: request.TurnID, - CorrelationID: request.CorrelationID, - Seq: seq, - CreatedAt: time.Now().UTC(), - Summary: summary, - Completed: completed, - Metadata: metadata, - } -} diff --git a/examples/harness/echo/main_test.go b/examples/harness/echo/main_test.go deleted file mode 100644 index d4a03d277..000000000 --- a/examples/harness/echo/main_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/orka-agents/orka/internal/harness" - "github.com/orka-agents/orka/internal/harness/conformance" -) - -func TestEchoHarnessCancelEndpointMatchesCapabilities(t *testing.T) { - s := newTestServer(behaviorSuccess) - srv := httptest.NewServer(s.handler()) - defer srv.Close() - client, err := harness.NewClient(srv.URL) - if err != nil { - t.Fatalf("NewClient() error = %v", err) - } - ctx := context.Background() - caps, err := client.Capabilities(ctx) - if err != nil { - t.Fatalf("Capabilities() error = %v", err) - } - if !caps.SupportsCancel { - t.Fatal("generic HTTP runtime should advertise cancellation") - } - request := validStartTurnRequest() - if _, err := client.StartTurn(ctx, request); err != nil { - t.Fatalf("StartTurn() error = %v", err) - } - cancelled, err := client.CancelTurn(ctx, harness.CancelTurnRequest{ - Version: harness.ProtocolVersion, - Namespace: request.Namespace, - TaskName: request.TaskName, - SessionName: request.SessionName, - RuntimeSessionID: request.RuntimeSessionID, - TurnID: request.TurnID, - CorrelationID: request.CorrelationID, - Reason: "test", - }) - if err != nil { - t.Fatalf("CancelTurn() error = %v", err) - } - if !cancelled.Accepted || - cancelled.TurnID != request.TurnID || - cancelled.RuntimeSessionID != request.RuntimeSessionID { - t.Fatalf("CancelTurn() = %#v", cancelled) - } -} - -func TestEchoHarnessRejectsDuplicateStartTurn(t *testing.T) { - s := newTestServer(behaviorSuccess) - srv := httptest.NewServer(s.handler()) - defer srv.Close() - client, err := harness.NewClient(srv.URL) - if err != nil { - t.Fatalf("NewClient() error = %v", err) - } - request := validStartTurnRequest() - if _, err := client.StartTurn(context.Background(), request); err != nil { - t.Fatalf("first StartTurn() error = %v", err) - } - _, err = client.StartTurn(context.Background(), request) - if err == nil || !strings.Contains(err.Error(), "turn already exists") { - t.Fatalf("second StartTurn() error = %v, want duplicate rejection", err) - } -} - -func TestEchoHarnessRejectsCompletedStartTurn(t *testing.T) { - s := newTestServer(behaviorSuccess) - srv := httptest.NewServer(s.handler()) - defer srv.Close() - client, err := harness.NewClient(srv.URL) - if err != nil { - t.Fatalf("NewClient() error = %v", err) - } - request := validStartTurnRequest() - if _, err := client.StartTurn(context.Background(), request); err != nil { - t.Fatalf("StartTurn() error = %v", err) - } - if err := client.StreamFrames( - context.Background(), - request.TurnID, - 0, - func(harness.HarnessEventFrame) error { return nil }, - ); err != nil { - t.Fatalf("StreamFrames() error = %v", err) - } - _, err = client.StartTurn(context.Background(), request) - if err == nil || !strings.Contains(err.Error(), "turn already completed") { - t.Fatalf("completed StartTurn() error = %v, want tombstone rejection", err) - } -} - -func TestSupportLookupEndpoint(t *testing.T) { - s := newTestServer(behaviorSuccess) - srv := httptest.NewServer(s.handler()) - defer srv.Close() - resp, err := http.Post(srv.URL+"/lookup", "application/json", bytes.NewBufferString(`{"incident":"case-1"}`)) - if err != nil { - t.Fatalf("POST /lookup: %v", err) - } - defer resp.Body.Close() //nolint:errcheck - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d, want 200", resp.StatusCode) - } - var body map[string]any - if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { - t.Fatalf("decode body: %v", err) - } - if body["success"] != true { - t.Fatalf("body = %#v", body) - } -} - -func TestGenericHTTPRuntimeApprovalContinuation(t *testing.T) { - s := newTestServer(behaviorApprovalTool) - srv := httptest.NewServer(s.handler()) - defer srv.Close() - client, err := harness.NewClient(srv.URL) - if err != nil { - t.Fatalf("NewClient() error = %v", err) - } - request := validStartTurnRequest() - request.ToolExecutionMode = harness.ToolExecutionModeBrokered - request.Input.Tools = []harness.ToolDefinition{{ - Name: defaultWriteToolName(), - BrokeredClass: harness.BrokeredToolClassWrite, - Parameters: json.RawMessage(`{"type":"object"}`), - }} - if _, err := client.StartTurn(context.Background(), request); err != nil { - t.Fatalf("StartTurn() error = %v", err) - } - firstCtx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) - defer cancel() - var firstFrames []harness.HarnessEventFrame - _ = client.StreamFrames(firstCtx, request.TurnID, 0, func(frame harness.HarnessEventFrame) error { - firstFrames = append(firstFrames, frame) - return nil - }) - if len(firstFrames) != 4 || - firstFrames[2].Type != harness.FrameToolCallRequested || - firstFrames[len(firstFrames)-1].Type != harness.FrameRuntimeLog { - t.Fatalf("first frames = %#v, want tool request followed by waiting diagnostic", firstFrames) - } - continued, err := client.ContinueTurn(context.Background(), harness.ContinueTurnRequest{ - Version: harness.ProtocolVersion, - Namespace: request.Namespace, - TaskName: request.TaskName, - SessionName: request.SessionName, - RuntimeSessionID: request.RuntimeSessionID, - TurnID: request.TurnID, - CorrelationID: request.CorrelationID, - ToolResults: []harness.ToolCallResult{{ - Version: harness.ProtocolVersion, - RuntimeSessionID: request.RuntimeSessionID, - TurnID: request.TurnID, - ToolCallID: "tool-write-1", - IdempotencyKey: harness.ToolRequestIdempotencyKey(request.RuntimeSessionID, request.TurnID, "tool-write-1"), - Approved: true, - Output: json.RawMessage(`{"success":true,"data":{"dispatched":true}}`), - }}, - }) - if err != nil { - t.Fatalf("ContinueTurn() error = %v", err) - } - if !continued.Accepted { - t.Fatalf("ContinueTurn() = %#v, want accepted", continued) - } - var finalFrames []harness.HarnessEventFrame - if err := client.StreamFrames(context.Background(), request.TurnID, 4, func(frame harness.HarnessEventFrame) error { - finalFrames = append(finalFrames, frame) - return nil - }); err != nil { - t.Fatalf("StreamFrames(after continue) error = %v", err) - } - if len(finalFrames) != 2 || - finalFrames[0].Type != harness.FrameToolResultReceived || - finalFrames[1].Type != harness.FrameTurnCompleted { - t.Fatalf("final frames = %#v, want tool result then completion", finalFrames) - } -} - -func TestGenericHTTPRuntimePassesBrokeredReadConformance(t *testing.T) { - s := newTestServer(behaviorReadTool) - srv := httptest.NewServer(s.handler()) - defer srv.Close() - observed := conformance.Check(context.Background(), conformance.Target{BaseURL: srv.URL, ProbeTurn: true}) - if !observed.Passed { - t.Fatalf("observed conformance failed: %v", observed.Failures) - } - result := conformance.Check(context.Background(), conformance.Target{BaseURL: srv.URL, ProbeBrokeredRead: true}) - if !result.Passed { - t.Fatalf("conformance failed: %v", result.Failures) - } -} - -func TestGenericHTTPRuntimePassesBrokeredWriteConformance(t *testing.T) { - s := newTestServer(behaviorApprovalTool) - srv := httptest.NewServer(s.handler()) - defer srv.Close() - observed := conformance.Check(context.Background(), conformance.Target{BaseURL: srv.URL, ProbeTurn: true}) - if !observed.Passed { - t.Fatalf("observed conformance failed: %v", observed.Failures) - } - result := conformance.Check(context.Background(), conformance.Target{BaseURL: srv.URL, ProbeBrokeredWrite: true}) - if !result.Passed { - t.Fatalf("conformance failed: %v", result.Failures) - } -} - -func newTestServer(behavior string) *server { - return &server{ - runtimeName: "orka-generic-http-runtime", - behavior: normalizeBehavior(behavior), - turns: map[harness.HarnessTurnID]*turnState{}, - completedTurns: map[harness.HarnessTurnID]struct{}{}, - } -} - -func (s *server) handler() http.Handler { - mux := http.NewServeMux() - mux.HandleFunc(harness.HealthPath, s.health) - mux.HandleFunc(harness.CapabilitiesPath, s.capabilities) - mux.HandleFunc(harness.TurnsPath, s.startTurn) - mux.HandleFunc(harness.TurnsPath+"/", s.turn) - mux.HandleFunc("/lookup", s.supportLookup) - return mux -} - -func validStartTurnRequest() harness.StartTurnRequest { - return harness.StartTurnRequest{ - Version: harness.ProtocolVersion, - Namespace: "default", - TaskName: "task", - SessionName: "session", - RuntimeSessionID: "runtime", - TurnID: "turn", - CorrelationID: "corr", - Deadline: time.Now().UTC().Add(time.Minute), - AuthIdentity: harness.AuthIdentity{Subject: "user:test"}, - Input: harness.TurnInput{Prompt: "hello"}, - } -} diff --git a/examples/iterative-review/coder-agent.yaml b/examples/iterative-review/coder-agent.yaml index 7d52465a3..8d77f376a 100644 --- a/examples/iterative-review/coder-agent.yaml +++ b/examples/iterative-review/coder-agent.yaml @@ -20,5 +20,7 @@ spec: inline: | You are a coding agent. Write clean, well-tested code. If you receive FEEDBACK FROM REVIEW, address all feedback items. + Do not commit, change Git configuration/remotes, push, or create a pull request. + Orka validates and publishes the final tree through a separate clean-room service. secretRef: name: claude-credentials diff --git a/examples/iterative-review/coordinator-agent.yaml b/examples/iterative-review/coordinator-agent.yaml index 34d707399..5c92fec6c 100644 --- a/examples/iterative-review/coordinator-agent.yaml +++ b/examples/iterative-review/coordinator-agent.yaml @@ -11,26 +11,30 @@ spec: inline: | You are a coordinator agent. Follow this protocol: 1. READ the repository details from the task prompt: - - gitRepo - - branch - - gitSecretRef - - pushBranch - 2. DELEGATE implementation to the coder agent with that workspace config. - The coder must work on the requested pushBranch. + - gitRepo and branch + - readCredentialRef + - publicationGitRepo and publicationCredentialRef + - pushBranch and prBaseBranch + 2. DELEGATE implementation to the coder agent with workspace.intent = write + and the complete source/publication configuration. The coder edits files; + Orka's clean-room publisher owns commit creation and branch delivery. 3. WAIT for the coder's result. 4. DELEGATE review to the reviewer agent with prior_task = the coder task name. 5. WAIT for the reviewer's verdict. 6. IF verdict == "CHANGES_NEEDED" AND iteration < 3: DELEGATE fix to the coder agent with prior_task + feedback. - Reuse the same workspace details and pushBranch. + Reuse the same workspace details and claimed pushBranch. + Require a verified delivery receipt before continuing. Go to step 3. - 7. IF verdict == "APPROVED": + 7. IF verdict == "APPROVED" and the latest coder delivery is verified: Call create_pull_request with: - task_name = the coder task name - head_branch = pushBranch from the task prompt - base_branch = branch from the task prompt - title/body that summarize the completed work - 8. REPORT the final result with the PR URL. + 8. REPORT the final result with the PR URL and verified branch SHA. + + Never ask an ACP runtime to commit, push, run gh, or create the PR itself. coordination: enabled: true maxDepth: 3 diff --git a/examples/iterative-review/iterative-task.yaml b/examples/iterative-review/iterative-task.yaml index 08bdd64b8..539f7b00c 100644 --- a/examples/iterative-review/iterative-task.yaml +++ b/examples/iterative-review/iterative-task.yaml @@ -12,8 +12,11 @@ spec: Repository details for delegated agent tasks: - gitRepo: https://github.com/example/api.git - branch: main - - gitSecretRef: git-credentials + - readCredentialRef: repository-read + - publicationGitRepo: https://github.com/example/api.git + - publicationCredentialRef: repository-publish - pushBranch: feature/jwt-auth + - prBaseBranch: main Requirements: - Login endpoint with username/password diff --git a/examples/iterative-review/reviewer-agent.yaml b/examples/iterative-review/reviewer-agent.yaml index 9d14373b0..5f22e88bf 100644 --- a/examples/iterative-review/reviewer-agent.yaml +++ b/examples/iterative-review/reviewer-agent.yaml @@ -16,7 +16,7 @@ spec: name: claude-sonnet-4-20250514 systemPrompt: inline: | - You are a code reviewer. Review the code changes in the workspace. + You are a code reviewer. Review the workspace supplied by Orka without modifying it. Respond with a clear verdict: - "APPROVED" if the code is ready - "CHANGES_NEEDED" with specific feedback if changes are required diff --git a/examples/support-escalation-runtime-demo/README.md b/examples/support-escalation-runtime-demo/README.md deleted file mode 100644 index 89474fdcd..000000000 --- a/examples/support-escalation-runtime-demo/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Support escalation bring-your-own AgentRuntime demo - -This non-Fibey scenario validates the generic bring-your-own agent runtime story in a support escalation domain. - -The workflow is intentionally the same shape as the Fibey demo: - -```text -Task/support-escalation-demo - -> Agent/support-remote-investigator - -> AgentRuntime/support-http-runtime - -> generic HTTP remote execution backend - -> Orka-brokered Tool/support-ticket-lookup -``` - -Security invariant: the remote runtime does not receive support-system credentials. It can request `support-ticket-lookup`; Orka validates policy, executes the Tool CRD, records events, and returns only the brokered result. - -## Run - -Build/load the generic HTTP fixture image first: - -```bash -docker build -t ghcr.io/orka-agents/orka/example-echo-harness:latest -f examples/harness/echo/Dockerfile . -kind load docker-image ghcr.io/orka-agents/orka/example-echo-harness:latest --name -``` - -Apply the demo: - -1. Create a per-cluster runtime bearer Secret named `support-http-runtime-token` - with data key `token`, label `orka.ai/agent-runtime-auth: "true"`, label - `orka.ai/agent-runtime-name: support-http-runtime`, and annotation - `orka.ai/agent-runtime-endpoint: http://support-http-runtime.default.svc.cluster.local:8080`. - Generate the bearer value outside the repository; do not commit it. -2. Apply the demo: - - ```bash - kubectl apply -k examples/support-escalation-runtime-demo - kubectl wait --for=condition=Ready agentruntime/support-http-runtime --timeout=60s - kubectl get task support-escalation-demo -o yaml - ``` - -The checked-in `Tool/support-ticket-lookup` points at the included mock `support-tool` service. Replace that service with a real read-only support lookup service for a live demo. The AgentRuntime and Orka task flow remain unchanged when swapping backends or domains. - -## Brokered write variant - -To exercise approval-gated writes in this domain, add a write-class Tool such as `support-escalate-case`, include it in `Task.spec.agentRuntime.allowedTools`, and set the fixture behavior to `approval-tool`. Orka will emit `ApprovalRequested`, execute the write Tool only after approval, and continue the remote runtime with the approved/declined result. diff --git a/examples/support-escalation-runtime-demo/agent.yaml b/examples/support-escalation-runtime-demo/agent.yaml deleted file mode 100644 index 6f21fe144..000000000 --- a/examples/support-escalation-runtime-demo/agent.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: Agent -metadata: - name: support-remote-investigator -spec: - runtime: - runtimeRef: - name: support-http-runtime - systemPrompt: - inline: | - You are a support escalation investigator. Use Orka-brokered tools for evidence gathering and summarize the customer's likely issue and next safe action. diff --git a/examples/support-escalation-runtime-demo/agentruntime.yaml b/examples/support-escalation-runtime-demo/agentruntime.yaml deleted file mode 100644 index b87257682..000000000 --- a/examples/support-escalation-runtime-demo/agentruntime.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: AgentRuntime -metadata: - name: support-http-runtime -spec: - contractVersion: orka.harness.v1 - deployment: - mode: external-endpoint - endpoint: http://support-http-runtime.default.svc.cluster.local:8080 - clientAuth: - bearerTokenSecretRef: - name: support-http-runtime-token - key: token - capabilities: - toolExecutionModes: - - observed - - brokered - brokeredToolClasses: - - read - supportsCancel: true - supportsRuntimeSessions: true - supportsContinuation: true diff --git a/examples/support-escalation-runtime-demo/kustomization.yaml b/examples/support-escalation-runtime-demo/kustomization.yaml deleted file mode 100644 index 0888f4dc7..000000000 --- a/examples/support-escalation-runtime-demo/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -resources: -- mock-http-runtime-service.yaml -- mock-support-tool-service.yaml -- tools.yaml -- agentruntime.yaml -- agent.yaml -- task.yaml diff --git a/examples/support-escalation-runtime-demo/mock-http-runtime-service.yaml b/examples/support-escalation-runtime-demo/mock-http-runtime-service.yaml deleted file mode 100644 index a93010ad1..000000000 --- a/examples/support-escalation-runtime-demo/mock-http-runtime-service.yaml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: support-http-runtime - labels: - app.kubernetes.io/name: support-http-runtime -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: support-http-runtime - template: - metadata: - labels: - app.kubernetes.io/name: support-http-runtime - spec: - containers: - - name: harness - image: ghcr.io/orka-agents/orka/example-echo-harness:latest - imagePullPolicy: IfNotPresent - env: - - name: ORKA_REMOTE_HTTP_RUNTIME_ADDR - value: :8080 - - name: ORKA_REMOTE_HTTP_RUNTIME_NAME - value: support-http-runtime - # Switch to read-tool or approval-tool to exercise brokered read/write protocol paths. - - name: ORKA_REMOTE_HTTP_RUNTIME_BEHAVIOR - value: read-tool - - name: ORKA_REMOTE_HTTP_RUNTIME_READ_TOOL_NAME - value: support-ticket-lookup - - name: ORKA_REMOTE_HTTP_RUNTIME_BEARER_TOKEN - valueFrom: - secretKeyRef: - name: support-http-runtime-token - key: token - ports: - - name: http - containerPort: 8080 - readinessProbe: - httpGet: - path: /v1/health - port: http - periodSeconds: 3 ---- -apiVersion: v1 -kind: Service -metadata: - name: support-http-runtime -spec: - selector: - app.kubernetes.io/name: support-http-runtime - ports: - - name: http - port: 8080 - targetPort: http diff --git a/examples/support-escalation-runtime-demo/mock-support-tool-service.yaml b/examples/support-escalation-runtime-demo/mock-support-tool-service.yaml deleted file mode 100644 index bf04552f0..000000000 --- a/examples/support-escalation-runtime-demo/mock-support-tool-service.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: support-tool - labels: - app.kubernetes.io/name: support-tool -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: support-tool - template: - metadata: - labels: - app.kubernetes.io/name: support-tool - spec: - containers: - - name: support-tool - image: ghcr.io/orka-agents/orka/example-echo-harness:latest - imagePullPolicy: IfNotPresent - env: - - name: ORKA_REMOTE_HTTP_RUNTIME_ADDR - value: :8080 - - name: ORKA_REMOTE_HTTP_RUNTIME_NAME - value: support-tool - ports: - - name: http - containerPort: 8080 - readinessProbe: - httpGet: - path: /v1/health - port: http - periodSeconds: 3 ---- -apiVersion: v1 -kind: Service -metadata: - name: support-tool -spec: - selector: - app.kubernetes.io/name: support-tool - ports: - - name: http - port: 8080 - targetPort: http diff --git a/examples/support-escalation-runtime-demo/task.yaml b/examples/support-escalation-runtime-demo/task.yaml deleted file mode 100644 index 789f6b015..000000000 --- a/examples/support-escalation-runtime-demo/task.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: core.orka.ai/v1alpha1 -kind: Task -metadata: - name: support-escalation-demo -spec: - type: agent - agentRef: - name: support-remote-investigator - agentRuntime: - allowedTools: - - support-ticket-lookup - prompt: | - Customer ACME-42 reports intermittent checkout failures after a payment gateway migration. - Gather evidence through brokered support tools and recommend the safest next action. diff --git a/examples/support-escalation-runtime-demo/tools.yaml b/examples/support-escalation-runtime-demo/tools.yaml deleted file mode 100644 index fae8053bb..000000000 --- a/examples/support-escalation-runtime-demo/tools.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Example brokered read Tool CRD. Replace the URL with a real support/ticket lookup service. -# Remote runtimes only request this tool; Orka owns execution and any downstream credentials. -apiVersion: core.orka.ai/v1alpha1 -kind: Tool -metadata: - name: support-ticket-lookup -spec: - description: Look up sanitized support ticket and service health evidence for a customer escalation. - brokeredToolClass: read - parameters: - type: object - properties: - incident: - type: string - description: Support incident or customer identifier. - required: - - incident - http: - url: http://support-tool.default.svc.cluster.local:8080/lookup - method: POST diff --git a/go.mod b/go.mod index 1b63a3a1c..3295f8964 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,9 @@ require ( github.com/anthropics/anthropic-sdk-go v1.61.0 github.com/charmbracelet/glamour v1.0.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/github/copilot-sdk/go v0.1.25 + github.com/distribution/reference v0.6.0 github.com/go-logr/logr v1.4.4 github.com/gofiber/fiber/v3 v3.4.0 - github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 github.com/lestrrat-go/jwx/v3 v3.1.1 github.com/onsi/ginkgo/v2 v2.32.0 @@ -31,6 +30,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 go.yaml.in/yaml/v3 v3.0.4 + golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 golang.org/x/time v0.15.0 google.golang.org/grpc v1.82.1 @@ -40,6 +40,7 @@ require ( k8s.io/apiextensions-apiserver v0.36.3 k8s.io/apimachinery v0.36.3 k8s.io/client-go v0.36.3 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 modernc.org/sqlite v1.53.0 sigs.k8s.io/agent-sandbox v0.5.3 sigs.k8s.io/controller-runtime v0.24.1 @@ -125,6 +126,7 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -159,7 +161,6 @@ require ( golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect golang.org/x/term v0.45.0 // indirect golang.org/x/text v0.40.0 // indirect golang.org/x/tools v0.47.0 // indirect @@ -174,7 +175,6 @@ require ( k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/streaming v0.36.3 // indirect - k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect modernc.org/libc v1.73.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index ad95dd32d..c7235e099 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= @@ -83,8 +85,6 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/github/copilot-sdk/go v0.1.25 h1:SJ/jSoesbpjDEBcvMkoCG+xITvgvnhxnd6oJdmNQnOs= -github.com/github/copilot-sdk/go v0.1.25/go.mod h1:qc2iEF7hdO8kzSvbyGvrcGhuk2fzdW4xTtT0+1EH2ts= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -153,8 +153,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= -github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -236,6 +234,8 @@ github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/openai/openai-go/v3 v3.41.0 h1:9GkxcN02U5NG0WGdQjZ0cTSu/pMXEyzL2LfF0ruZCck= github.com/openai/openai-go/v3 v3.41.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= diff --git a/hack/agent-substrate/README.md b/hack/agent-substrate/README.md new file mode 100644 index 000000000..0bd1d2bc0 --- /dev/null +++ b/hack/agent-substrate/README.md @@ -0,0 +1,107 @@ +# Agent Substrate evaluation patches + +Orka pins Agent Substrate at +`b80031d260959b1fc5c6f61e3099fe2a6d368af1` for local/CI evaluation. The +installer clones that immutable revision and applies the reviewed patches in +this directory before building Substrate. These are evaluation-only +compatibility and hardening patches; Orka does not install or manage Agent +Substrate in production. Patch-set changes are not rolled through a live worker +fleet: the retained-cluster marker binds the exact patch blobs and requires a +full cluster recreate, so mixed-version snapshot readers and rollback migration +are intentionally outside this local/CI evaluation contract. + +Each patch is fail-closed: + +- `scripts/agent-substrate-e2e.sh` verifies the exact upstream Git blob for + every existing source file the patch changes. +- The patch must apply with `--whitespace=error-all` and reverse-apply cleanly. +- The parsed patch path set must exactly match its declared scope. +- Focused upstream package tests run before the Kind cluster is created. +- A different `SUBSTRATE_REF` is rejected unless the blobs and patches are + explicitly reviewed and updated together. + +## Patch set + +### `atelet-root-supervisor-capabilities.patch` + +Scopes `CAP_SETUID` and `CAP_SETGID` to the Orka workspace-agent supervisor and +keeps its extracted root filesystem traversable after the supervisor drops task +commands to UID/GID 1000. + +### `atenet-router-authorization-redaction.patch` + +Makes `atenet-router` request logging allowlist-only. Request metadata retains +only method, sanitized path, host/authority, and request ID; every other header +is discarded before its normal or `RawValue` content is read. Request-target +parsing keeps only the escaped path, including for legal absolute-form proxy +requests. Host/authority values are parsed independently and reject userinfo, +paths, queries, and fragments, so query credentials and absolute-URI userinfo +cannot reach logs or the status recorder. Upstream tests cover +`Authorization`, `Proxy-Authorization`, `Txn-Token`, `Cookie`, `X-API-Key`, +unknown `RawValue` headers, and query credentials. The patch also lowers Envoy's +`ext_proc`, router, and upstream component logging from debug to info in both +the static install manifest and the programmatic runner so the sidecar cannot +log raw request header tuples before the application-level allowlist boundary. + +### `ateom-runsc-delete-recovery.patch` + +Hardens the pinned `ateom-gvisor` checkpoint cleanup path: + +- prepares a durable recovery directory and points the ordinary checkpoint path + at it before `runsc checkpoint` writes any bytes; +- pins `flate-best-speed` so the stopped sandbox produces exactly one statefile, + then validates that file with runsc's own `statefile` reader before cleanup; +- keeps committed recovery in its original one-file format, then materializes a + separate upload view with a hard link to `checkpoint.img` plus explicitly + marked `pages.img`/`pages_meta.img` compatibility files. Restore removes only + those exact markers (including a surviving half of an interrupted cleanup) + while preserving native multi-file snapshots even when pages are empty; +- reconciles an interrupted commit by checking the pause-container state: a + valid statefile plus `stopped` is committed in place, while absent/partial + bytes may be discarded only when the sandbox is still `created` or `running`; +- trusts checkpoint bytes only after atomically writing a deterministic commit + inventory covering the statefile mode, size, and SHA-256 digest; +- stages the atomic commit temporary beside (not inside) the inventoried + recovery directory, so a crash cannot turn its orphan into an extra artifact; +- runs every direct `runsc list` under the PID-1 child-reaper read lock and + parses `runsc state` JSON from stdout separately from stderr diagnostics; +- restores the worker Pod network before fallible `runsc` state/delete cleanup + and treats an already-restored interface as an idempotent retry state; +- converges across partial container deletion by reusing the durable checkpoint + instead of checkpointing the stopped sandbox again; +- retries a failed `runsc delete --force` a bounded four times; +- after every failed delete, runs `runsc list --quiet` and accepts only an exact, + verified absence of the target container; +- fails closed if checkpoint/container/network state cannot be verified; +- includes deterministic fake-`runsc` tests for crash-after-checkpoint recovery, + two-call cleanup after retry exhaustion, exit-128-after-removal, transient + retry success, persistent failure, and verification failure. + +With `SUBSTRATE_E2E_EXTENDED=1`, the live E2E deletes an assigned worker and +verifies store/Deployment replacement, then installs a temporary fail-once +`runsc` delegator on a live replacement worker. The Actor must suspend through +the reviewed retry path, the original binary must be restored, and a subsequent +direct Actor lifecycle must route, execute, suspend, and delete without leaving +any Actor in `STATUS_SUSPENDING`. + +## Validation + +Fast repository checks: + +```bash +bash scripts/tests/agent-substrate-patches-test.sh +bash -n scripts/agent-substrate-e2e.sh hack/demos/cluster/install-substrate.sh +``` + +The complete Linux/Kind validation is destructive to a same-named Kind cluster: + +```bash +PATH="$(go env GOPATH)/bin:$PATH" \ +SUBSTRATE_E2E_EXTENDED=1 \ +KEEP_CLUSTER=1 \ +bash scripts/agent-substrate-e2e.sh +``` + +Review and update the source blob constants in `scripts/agent-substrate-e2e.sh` +only after inspecting the replacement upstream source and regenerating the +corresponding patch. diff --git a/hack/agent-substrate/atenet-router-authorization-redaction.patch b/hack/agent-substrate/atenet-router-authorization-redaction.patch new file mode 100644 index 000000000..d222e5825 --- /dev/null +++ b/hack/agent-substrate/atenet-router-authorization-redaction.patch @@ -0,0 +1,311 @@ +diff --git a/cmd/servers/atenet/app/router/envoyrunner.go b/cmd/servers/atenet/app/router/envoyrunner.go +index 8d38be29..593f4a9e 100644 +--- a/cmd/servers/atenet/app/router/envoyrunner.go ++++ b/cmd/servers/atenet/app/router/envoyrunner.go +@@ -170,7 +170,7 @@ func (r *envoyrunner) reconcileEnvoyDeployment(ctx context.Context) error { + "-c", + "/etc/envoy/envoy.yaml", + "--component-log-level", +- "upstream:debug,router:debug,ext_proc:debug", ++ "upstream:info,router:info,ext_proc:info", + }, + Ports: []corev1.ContainerPort{ + { +diff --git a/cmd/servers/atenet/app/router/extproc_in.go b/cmd/servers/atenet/app/router/extproc_in.go +index 31751184..7271d8cf 100644 +--- a/cmd/servers/atenet/app/router/extproc_in.go ++++ b/cmd/servers/atenet/app/router/extproc_in.go +@@ -17,20 +17,77 @@ package router + import ( + "fmt" + "net" ++ "net/url" + "strings" + + "github.com/agent-substrate/substrate/internal/resources" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + ) + ++const ( ++ invalidRequestPath = "[invalid]" ++ invalidRequestAuthority = "[invalid]" ++) ++ + type requestMetadata struct { + headers map[string]string + path string + host string + } + ++func isSafeRequestMetadataHeader(key string) bool { ++ switch key { ++ case ":method", ":path", ":authority", "host", "x-request-id": ++ return true ++ default: ++ return false ++ } ++} ++ ++func sanitizeRequestPath(value string) string { ++ if value == "*" { ++ return value ++ } ++ requestURI, err := url.ParseRequestURI(value) ++ if err != nil { ++ return invalidRequestPath ++ } ++ if requestURI.Opaque != "" { ++ // ParseRequestURI intentionally stores an absolute-form request target in ++ // Opaque. Parse it as a URL only to recover the escaped path; userinfo, ++ // authority, query, and fragment values remain excluded from metadata. ++ requestURI, err = url.Parse(value) ++ if err != nil || requestURI.Scheme == "" || requestURI.Host == "" { ++ return invalidRequestPath ++ } ++ } ++ path := requestURI.EscapedPath() ++ if path == "" { ++ return "/" ++ } ++ return path ++} ++ ++func sanitizeRequestAuthority(value string) string { ++ if value == "" { ++ return "" ++ } ++ authority, err := url.Parse("//" + value) ++ if err != nil || authority.Host == "" || authority.User != nil || authority.Path != "" || ++ authority.RawPath != "" || authority.RawQuery != "" || authority.Fragment != "" { ++ return invalidRequestAuthority ++ } ++ return authority.Host ++} ++ + func (m *requestMetadata) String() string { +- return fmt.Sprintf("%+v", *m) ++ return fmt.Sprintf( ++ "method=%q path=%q host=%q request_id=%q", ++ m.headers[":method"], ++ m.path, ++ m.host, ++ m.headers["x-request-id"], ++ ) + } + + func newRequestMetadata(headers []*corev3.HeaderValue) *requestMetadata { +@@ -40,18 +97,24 @@ func newRequestMetadata(headers []*corev3.HeaderValue) *requestMetadata { + + for _, h := range headers { + k := strings.ToLower(h.Key) ++ if !isSafeRequestMetadataHeader(k) { ++ continue ++ } ++ + val := h.Value + if val == "" && len(h.RawValue) > 0 { + val = string(h.RawValue) + } +- +- headersMap[k] = val + if k == ":path" { ++ val = sanitizeRequestPath(val) + path = val + } + if k == ":authority" || k == "host" { ++ val = sanitizeRequestAuthority(val) + host = val + } ++ ++ headersMap[k] = val + } + + return &requestMetadata{ +diff --git a/cmd/servers/atenet/app/router/extproc_in_test.go b/cmd/servers/atenet/app/router/extproc_in_test.go +index 09bb9a4c..e6d2e1e1 100644 +--- a/cmd/servers/atenet/app/router/extproc_in_test.go ++++ b/cmd/servers/atenet/app/router/extproc_in_test.go +@@ -15,14 +15,15 @@ + package router + + import ( +- "fmt" + "reflect" ++ "strings" + "testing" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + ) + + func TestExtractMetadata(t *testing.T) { ++ absoluteFormTarget := "https://example.com/api/v1/a%2Fb?sample=placeholder" + tests := []struct { + name string + headers []*corev3.HeaderValue +@@ -31,13 +32,15 @@ func TestExtractMetadata(t *testing.T) { + wantHost string + }{ + { +- name: "basic path and authority", ++ name: "basic safe routing metadata", + headers: []*corev3.HeaderValue{ ++ {Key: ":method", Value: "GET"}, + {Key: ":path", Value: "/api/v1/test"}, + {Key: ":authority", Value: "example.com"}, + {Key: "X-Request-ID", Value: "req-123"}, + }, + wantHeaders: map[string]string{ ++ ":method": "GET", + ":path": "/api/v1/test", + ":authority": "example.com", + "x-request-id": "req-123", +@@ -76,29 +79,62 @@ func TestExtractMetadata(t *testing.T) { + wantHost: "authority.com", + }, + { +- name: "no authority or host headers", ++ name: "arbitrary headers are omitted", + headers: []*corev3.HeaderValue{ + {Key: ":path", Value: "/api/v1/test"}, + {Key: "x-something-else", Value: "custom-value"}, ++ {Key: "X-Request-ID", RawValue: []byte("req-from-raw-value")}, + }, + wantHeaders: map[string]string{ +- ":path": "/api/v1/test", +- "x-something-else": "custom-value", ++ ":path": "/api/v1/test", ++ "x-request-id": "req-from-raw-value", + }, + wantPath: "/api/v1/test", + wantHost: "", + }, + { +- name: "headers are lowercased", ++ name: "credential headers and query values are omitted", + headers: []*corev3.HeaderValue{ +- {Key: "UPPER-KEY", Value: "UPPER-VALUE"}, +- {Key: "camelCaseKey", Value: "camelValue"}, ++ {Key: ":method", Value: "POST"}, ++ {Key: ":path", RawValue: []byte("/api/v1/test?token=test-token-placeholder")}, ++ {Key: ":authority", Value: "example.com"}, ++ {Key: "X-Request-ID", Value: "req-credential-test"}, ++ {Key: "Authorization", Value: "Bearer test-auth-token"}, ++ {Key: "Proxy-Authorization", RawValue: []byte("Basic dummy")}, ++ {Key: "Txn-Token", Value: "gateway-token"}, ++ {Key: "Cookie", RawValue: []byte("session=sample")}, ++ {Key: "X-API-Key", Value: "fake"}, ++ {Key: "X-Unrecognized-Raw", RawValue: []byte("not-a-real")}, + }, + wantHeaders: map[string]string{ +- "upper-key": "UPPER-VALUE", +- "camelcasekey": "camelValue", ++ ":method": "POST", ++ ":path": "/api/v1/test", ++ ":authority": "example.com", ++ "x-request-id": "req-credential-test", + }, +- wantPath: "", ++ wantPath: "/api/v1/test", ++ wantHost: "example.com", ++ }, ++ { ++ name: "absolute-form request target preserves only escaped path", ++ headers: []*corev3.HeaderValue{ ++ {Key: ":path", Value: absoluteFormTarget}, ++ }, ++ wantHeaders: map[string]string{ ++ ":path": "/api/v1/a%2Fb", ++ }, ++ wantPath: "/api/v1/a%2Fb", ++ wantHost: "", ++ }, ++ { ++ name: "invalid request target fails closed", ++ headers: []*corev3.HeaderValue{ ++ {Key: ":path", Value: "not a valid request URI"}, ++ }, ++ wantHeaders: map[string]string{ ++ ":path": invalidRequestPath, ++ }, ++ wantPath: invalidRequestPath, + wantHost: "", + }, + } +@@ -120,18 +156,56 @@ func TestExtractMetadata(t *testing.T) { + } + } + +-func TestRequestMetadata_String(t *testing.T) { ++func TestSanitizeRequestAuthority(t *testing.T) { ++ userinfoFixture := string([]byte{0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x3a, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d}) ++ for _, test := range []struct { ++ name string ++ value string ++ want string ++ }{ ++ {name: "host", value: "example.com", want: "example.com"}, ++ {name: "host with port", value: "example.com:8443", want: "example.com:8443"}, ++ {name: "userinfo", value: userinfoFixture, want: invalidRequestAuthority}, ++ {name: "query", value: "example.com?sample=placeholder", want: invalidRequestAuthority}, ++ } { ++ t.Run(test.name, func(t *testing.T) { ++ if got := sanitizeRequestAuthority(test.value); got != test.want { ++ t.Fatalf("sanitizeRequestAuthority() = %q, want %q", got, test.want) ++ } ++ }) ++ } ++} ++ ++func TestRequestMetadataStringOnlyIncludesSafeRoutingMetadata(t *testing.T) { + headers := []*corev3.HeaderValue{ +- {Key: ":path", Value: "/api/v1/test"}, ++ {Key: ":method", Value: "POST"}, ++ {Key: ":path", RawValue: []byte("/api/v1/test")}, + {Key: ":authority", Value: "example.com"}, ++ {Key: "X-Request-ID", RawValue: []byte("req-123")}, ++ {Key: "Authorization", Value: "Bearer secret-token"}, ++ {Key: "Proxy-Authorization", RawValue: []byte("Basic decoy-token")}, ++ {Key: "Txn-Token", Value: "matrix_qa_e2ee_cli_gateway"}, ++ {Key: "Cookie", RawValue: []byte("session=changeme")}, ++ {Key: "X-API-Key", Value: "redacted"}, ++ {Key: "X-Unrecognized-Raw", RawValue: []byte("token-oversized")}, + } +- m := newRequestMetadata(headers) +- str := m.String() +- if str == "" { +- t.Errorf("expected non-empty string from String()") ++ metadata := newRequestMetadata(headers) ++ got := metadata.String() ++ want := `method="POST" path="/api/v1/test" host="example.com" request_id="req-123"` ++ if got != want { ++ t.Fatalf("String() = %q, want %q", got, want) + } +- if !reflect.DeepEqual(str, fmt.Sprintf("%+v", *m)) { +- t.Errorf("String() = %q, want %q", str, fmt.Sprintf("%+v", *m)) ++ for _, sensitive := range []string{ ++ "secret-token", ++ "decoy-token", ++ "matrix_qa_e2ee_cli_gateway", ++ "changeme", ++ "redacted", ++ "token-oversized", ++ } { ++ if strings.Contains(got, sensitive) { ++ t.Fatalf("String() leaked sensitive value %q: %q", sensitive, got) ++ } + } + } + +diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml +index e309cad0..74486a9b 100644 +--- a/manifests/ate-install/atenet-router.yaml ++++ b/manifests/ate-install/atenet-router.yaml +@@ -146,7 +146,7 @@ spec: + - "-c" + - "/etc/envoy/envoy.yaml" + - "--component-log-level" +- - "upstream:debug,router:debug,ext_proc:debug" ++ - "upstream:info,router:info,ext_proc:info" + ports: + - name: http + containerPort: 8080 diff --git a/hack/agent-substrate/ateom-runsc-delete-recovery.patch b/hack/agent-substrate/ateom-runsc-delete-recovery.patch new file mode 100644 index 000000000..6f8fa6ada --- /dev/null +++ b/hack/agent-substrate/ateom-runsc-delete-recovery.patch @@ -0,0 +1,2018 @@ +diff --git a/cmd/servers/ateom-gvisor/ateom-gvisor.go b/cmd/servers/ateom-gvisor/ateom-gvisor.go +index 7d79dd0a..65b719ed 100644 +--- a/cmd/servers/ateom-gvisor/ateom-gvisor.go ++++ b/cmd/servers/ateom-gvisor/ateom-gvisor.go +@@ -16,12 +16,19 @@ package main + + import ( + "context" ++ "crypto/sha256" ++ "encoding/json" ++ "errors" + "flag" + "fmt" ++ "io" + "log/slog" + "net" + "os" ++ "path/filepath" + "runtime" ++ "sort" ++ "strings" + "sync" + "time" + +@@ -270,58 +277,833 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec + if err := os.MkdirAll(checkpointPath, 0o700); err != nil { + return nil, fmt.Errorf("while creating checkpoint directory: %w", err) + } ++ recoveryPath := filepath.Join( ++ ateompath.PIDFileDir(req.GetActorTemplateNamespace(), req.GetActorTemplateName(), req.GetActorId()), ++ checkpointRecoveryDirName, ++ ) ++ applicationContainers := make([]string, 0, len(req.GetSpec().GetContainers())) ++ for _, ctr := range req.GetSpec().GetContainers() { ++ applicationContainers = append(applicationContainers, ctr.GetName()) ++ } ++ ++ if err := checkpointAndCleanup( ++ ctx, ++ rcmd, ++ applicationContainers, ++ checkpointPath, ++ recoveryPath, ++ s.restorePodNetwork, ++ ); err != nil { ++ return nil, err ++ } ++ ++ s.actorLogger.EmitLifecycleLog("Actor checkpointed", req.GetActorId(), req.GetActorTemplateName(), req.GetActorTemplateNamespace()) ++ ++ return nil, nil ++} ++ ++const ( ++ checkpointRecoveryDirName = ".checkpoint-delete-recovery-v1" ++ checkpointRecoveryManifestName = ".checkpoint-delete-recovery-containers" ++ checkpointRecoveryCommitName = ".checkpoint-delete-recovery-committed" ++ checkpointRecoveryPreparePrefix = ".checkpoint-delete-recovery-prepare-" ++ checkpointRecoveryCommitPrefix = ".checkpoint-delete-recovery-commit-" ++ checkpointPagesCompatibilityMarker = "orka-compressed-pages-placeholder-v1\n" ++ checkpointPagesMetadataCompatibilityMarker = "orka-compressed-pages-metadata-placeholder-v1\n" ++) ++ ++type checkpointRuntime interface { ++ cmdCheckpoint(context.Context, string, string) error ++ cmdValidateCheckpoint(context.Context, string) error ++ containerStatus(context.Context, string) (string, error) ++ cmdState(context.Context, string) error ++ cmdDelete(context.Context, string) error ++ containerNames(context.Context) (map[string]struct{}, error) ++} ++ ++type podNetworkOps struct { ++ podEth0Present func(context.Context) (bool, error) ++ interiorEth0Present func(context.Context) (bool, error) ++ moveInteriorEth0ToPod func(context.Context) error ++} ++ ++func checkpointAndCleanup( ++ ctx context.Context, ++ rcmd checkpointRuntime, ++ applicationContainers []string, ++ checkpointPath string, ++ recoveryPath string, ++ restoreNetwork func(context.Context) error, ++) error { ++ orderedContainers, expectedContainers, err := expectedCheckpointContainers(applicationContainers) ++ if err != nil { ++ return fmt.Errorf("invalid checkpoint container set: %w", err) ++ } ++ ++ presentContainers, err := rcmd.containerNames(ctx) ++ if err != nil { ++ return fmt.Errorf("failed closed while listing containers before checkpoint cleanup: %w", err) ++ } ++ if err := validatePresentContainers(presentContainers, expectedContainers); err != nil { ++ return err ++ } ++ ++ recoveryState, err := inspectCheckpointRecovery(recoveryPath, expectedContainers) ++ if err != nil { ++ return fmt.Errorf("failed closed while validating checkpoint recovery: %w", err) ++ } ++ switch recoveryState { ++ case checkpointRecoveryCommitted: ++ if err := restoreCheckpointRecovery(checkpointPath, recoveryPath, expectedContainers); err != nil { ++ return fmt.Errorf("failed closed while restoring checkpoint recovery: %w", err) ++ } ++ case checkpointRecoveryPrepared: ++ // A prepared recovery has no atomic commit record. Reconcile it against ++ // runsc itself before deciding whether bytes are canonical: ++ // * a parseable single-file checkpoint plus a stopped sandbox is a ++ // completed checkpoint whose commit record was interrupted; ++ // * a running/created sandbox is still safe to checkpoint again, so ++ // discard any absent or partial bytes; ++ // * an invalid checkpoint paired with a stopped sandbox is uncertain ++ // and must fail closed without deleting containers. ++ checkpointErr := rcmd.cmdValidateCheckpoint(ctx, recoveryPath) ++ status, statusErr := rcmd.containerStatus(ctx, "pause") ++ if statusErr != nil { ++ return fmt.Errorf("failed closed while reconciling prepared checkpoint state: %w", statusErr) ++ } ++ switch status { ++ case "stopped": ++ if checkpointErr != nil { ++ return fmt.Errorf("failed closed: stopped sandbox has an invalid uncommitted checkpoint: %w", checkpointErr) ++ } ++ if err := commitCheckpointRecovery(recoveryPath, expectedContainers); err != nil { ++ return fmt.Errorf("failed closed while committing recovered checkpoint: %w", err) ++ } ++ if err := restoreCheckpointRecovery(checkpointPath, recoveryPath, expectedContainers); err != nil { ++ return fmt.Errorf("failed closed while restoring recovered checkpoint: %w", err) ++ } ++ case "created", "running": ++ if !sameContainerSet(presentContainers, expectedContainers) { ++ return fmt.Errorf( ++ "failed closed: partial container state with an uncommitted checkpoint (present=%s expected=%s)", ++ formatContainerSet(presentContainers), ++ formatContainerSet(expectedContainers), ++ ) ++ } ++ if err := resetPreparedCheckpointRecovery(checkpointPath, recoveryPath, expectedContainers); err != nil { ++ return fmt.Errorf("failed closed while resetting uncommitted checkpoint recovery: %w", err) ++ } ++ if err := checkpointFreshSandbox(ctx, rcmd, checkpointPath, recoveryPath, expectedContainers); err != nil { ++ return err ++ } ++ default: ++ return fmt.Errorf("failed closed: unsupported pause container state %q while reconciling checkpoint", status) ++ } ++ case checkpointRecoveryAbsent: ++ if !sameContainerSet(presentContainers, expectedContainers) { ++ return fmt.Errorf( ++ "failed closed: partial container state without complete checkpoint recovery (present=%s expected=%s)", ++ formatContainerSet(presentContainers), ++ formatContainerSet(expectedContainers), ++ ) ++ } ++ if err := checkpointFreshSandbox(ctx, rcmd, checkpointPath, recoveryPath, expectedContainers); err != nil { ++ return err ++ } ++ default: ++ return fmt.Errorf("failed closed: unknown checkpoint recovery state %d", recoveryState) ++ } ++ ++ // Restore the worker Pod network before fallible runsc cleanup. The operation ++ // is state-aware so a retry accepts eth0 only when it is already in the Pod ++ // namespace and absent from the interior namespace. ++ if err := restoreNetwork(ctx); err != nil { ++ return fmt.Errorf("while restoring worker Pod network: %w", err) ++ } + +- // Checkpoint pause container (root of the sandbox) ++ presentContainers, err = rcmd.containerNames(ctx) ++ if err != nil { ++ return fmt.Errorf("failed closed while listing containers before state inspection: %w", err) ++ } ++ if err := validatePresentContainers(presentContainers, expectedContainers); err != nil { ++ return err ++ } ++ ++ // Check state only for containers that remain. A previous cleanup attempt may ++ // already have removed an application container or the pause container. ++ for _, containerName := range orderedContainers { ++ if _, present := presentContainers[containerName]; !present { ++ continue ++ } ++ if err := rcmd.cmdState(ctx, containerName); err != nil { ++ return fmt.Errorf("while checking state of %q container: %w", containerName, err) ++ } ++ } ++ ++ // Delete all application containers that remain. ++ for _, containerName := range orderedContainers[1:] { ++ if _, present := presentContainers[containerName]; !present { ++ continue ++ } ++ if err := rcmd.cmdDelete(ctx, containerName); err != nil { ++ return fmt.Errorf("while deleting %q application container: %w", containerName, err) ++ } ++ } ++ ++ // Delete the pause container last when it remains. ++ if _, present := presentContainers["pause"]; present { ++ if err := rcmd.cmdDelete(ctx, "pause"); err != nil { ++ return fmt.Errorf("while deleting pause container: %w", err) ++ } ++ } ++ ++ presentContainers, err = rcmd.containerNames(ctx) ++ if err != nil { ++ return fmt.Errorf("failed closed while verifying checkpoint cleanup: %w", err) ++ } ++ if err := validatePresentContainers(presentContainers, expectedContainers); err != nil { ++ return err ++ } ++ if len(presentContainers) != 0 { ++ return fmt.Errorf("failed closed: containers remain after checkpoint cleanup: %s", formatContainerSet(presentContainers)) ++ } ++ if err := materializeCheckpointTransport(checkpointPath, recoveryPath); err != nil { ++ return fmt.Errorf("failed closed while materializing checkpoint transport: %w", err) ++ } ++ ++ // Keep the recovery copy until atelet uploads the snapshot and resets the ++ // actor directories. This also makes a retry after an upload failure safe. ++ return nil ++} ++ ++func checkpointFreshSandbox( ++ ctx context.Context, ++ rcmd checkpointRuntime, ++ checkpointPath string, ++ recoveryPath string, ++ expectedContainers map[string]struct{}, ++) error { ++ // Compression is pinned in cmdCheckpoint so runsc produces one statefile ++ // that its own statefile command can validate before the stopped sandbox is ++ // treated as canonical. ++ if err := prepareCheckpointRecovery(checkpointPath, recoveryPath, expectedContainers); err != nil { ++ return fmt.Errorf("failed closed while preparing durable checkpoint recovery: %w", err) ++ } + if err := rcmd.cmdCheckpoint(ctx, "pause", checkpointPath); err != nil { +- return nil, fmt.Errorf("while checkpointing pause: %w", err) ++ return fmt.Errorf("while checkpointing pause into durable recovery: %w", err) + } ++ if err := rcmd.cmdValidateCheckpoint(ctx, recoveryPath); err != nil { ++ return fmt.Errorf("failed closed while validating runsc checkpoint: %w", err) ++ } ++ status, err := rcmd.containerStatus(ctx, "pause") ++ if err != nil { ++ return fmt.Errorf("failed closed while verifying checkpointed sandbox state: %w", err) ++ } ++ if status != "stopped" { ++ return fmt.Errorf("failed closed: checkpointed pause container state is %q, want stopped", status) ++ } ++ if err := commitCheckpointRecovery(recoveryPath, expectedContainers); err != nil { ++ return fmt.Errorf("failed closed while committing durable checkpoint recovery: %w", err) ++ } ++ return nil ++} + +- // Check state of all containers to mimic containerd. +- // +- // Without this, `runsc delete` occasionally throws an error. +- if err := rcmd.cmdState(ctx, "pause"); err != nil { +- return nil, fmt.Errorf("while checking state of pause container: %w", err) ++func expectedCheckpointContainers(applicationContainers []string) ([]string, map[string]struct{}, error) { ++ ordered := make([]string, 0, len(applicationContainers)+1) ++ ordered = append(ordered, "pause") ++ expected := map[string]struct{}{"pause": {}} ++ for _, containerName := range applicationContainers { ++ if containerName == "" { ++ return nil, nil, fmt.Errorf("application container name is empty") ++ } ++ if containerName == "pause" { ++ return nil, nil, fmt.Errorf("application container uses reserved name %q", containerName) ++ } ++ if _, duplicate := expected[containerName]; duplicate { ++ return nil, nil, fmt.Errorf("duplicate application container name %q", containerName) ++ } ++ expected[containerName] = struct{}{} ++ ordered = append(ordered, containerName) + } +- for _, ctr := range req.GetSpec().GetContainers() { +- if err := rcmd.cmdState(ctx, ctr.GetName()); err != nil { +- return nil, fmt.Errorf("while deleting %q application container: %w", ctr.GetName(), err) ++ return ordered, expected, nil ++} ++ ++func validatePresentContainers(present, expected map[string]struct{}) error { ++ for containerName := range present { ++ if _, ok := expected[containerName]; !ok { ++ return fmt.Errorf("failed closed: unexpected runsc container %q is present", containerName) + } + } ++ return nil ++} + +- // Delete all application containers +- for _, ctr := range req.GetSpec().GetContainers() { +- if err := rcmd.cmdDelete(ctx, ctr.GetName()); err != nil { +- return nil, fmt.Errorf("while deleting %q application container: %w", ctr.GetName(), err) ++func sameContainerSet(left, right map[string]struct{}) bool { ++ if len(left) != len(right) { ++ return false ++ } ++ for name := range left { ++ if _, ok := right[name]; !ok { ++ return false + } + } ++ return true ++} ++ ++func formatContainerSet(containers map[string]struct{}) string { ++ names := make([]string, 0, len(containers)) ++ for name := range containers { ++ names = append(names, name) ++ } ++ sort.Strings(names) ++ return strings.Join(names, ",") ++} ++ ++func checkpointRecoveryManifest(expected map[string]struct{}) []byte { ++ return []byte(formatContainerSet(expected) + "\n") ++} ++ ++type checkpointRecoveryState int ++ ++const ( ++ checkpointRecoveryAbsent checkpointRecoveryState = iota ++ checkpointRecoveryPrepared ++ checkpointRecoveryCommitted ++) ++ ++func inspectCheckpointRecovery(recoveryPath string, expected map[string]struct{}) (checkpointRecoveryState, error) { ++ info, err := os.Lstat(recoveryPath) ++ if errors.Is(err, os.ErrNotExist) { ++ return checkpointRecoveryAbsent, nil ++ } ++ if err != nil { ++ return checkpointRecoveryAbsent, fmt.Errorf("while stating recovery directory: %w", err) ++ } ++ if !info.IsDir() { ++ return checkpointRecoveryAbsent, fmt.Errorf("recovery path %q is not a directory", recoveryPath) ++ } + +- // Delete pause container +- if err := rcmd.cmdDelete(ctx, "pause"); err != nil { +- return nil, fmt.Errorf("while deleting pause container: %w", err) ++ manifestPath := filepath.Join(recoveryPath, checkpointRecoveryManifestName) ++ manifestInfo, err := os.Lstat(manifestPath) ++ if err != nil { ++ return checkpointRecoveryAbsent, fmt.Errorf("while stating recovery manifest: %w", err) ++ } ++ if !manifestInfo.Mode().IsRegular() { ++ return checkpointRecoveryAbsent, fmt.Errorf("recovery manifest is not a regular file") ++ } ++ manifest, err := os.ReadFile(manifestPath) ++ if err != nil { ++ return checkpointRecoveryAbsent, fmt.Errorf("while reading recovery manifest: %w", err) ++ } ++ if string(manifest) != string(checkpointRecoveryManifest(expected)) { ++ return checkpointRecoveryAbsent, fmt.Errorf("recovery manifest does not match requested containers") + } + +- // Yoink eth0 back to the pod netns. +- podNetNS, err := netns.Get() ++ commitPath := filepath.Join(recoveryPath, checkpointRecoveryCommitName) ++ commitInfo, err := os.Lstat(commitPath) ++ if errors.Is(err, os.ErrNotExist) { ++ return checkpointRecoveryPrepared, nil ++ } + if err != nil { +- return nil, fmt.Errorf("while getting pod netns: %w", err) ++ return checkpointRecoveryAbsent, fmt.Errorf("while stating recovery commit: %w", err) + } +- err = netNSDo(ctx, s.interiorNetNS, func(ctx context.Context) error { +- eth0Link, err := netlink.LinkByName("eth0") ++ if !commitInfo.Mode().IsRegular() { ++ return checkpointRecoveryAbsent, fmt.Errorf("recovery commit is not a regular file") ++ } ++ commit, err := os.ReadFile(commitPath) ++ if err != nil { ++ return checkpointRecoveryAbsent, fmt.Errorf("while reading recovery commit: %w", err) ++ } ++ currentCommit, err := checkpointRecoveryCommit(recoveryPath) ++ if err != nil { ++ return checkpointRecoveryAbsent, err ++ } ++ if string(commit) != string(currentCommit) { ++ return checkpointRecoveryAbsent, fmt.Errorf("recovery checkpoint integrity does not match its commit record") ++ } ++ return checkpointRecoveryCommitted, nil ++} ++ ++func validateCheckpointRecovery(recoveryPath string, expected map[string]struct{}) (bool, error) { ++ state, err := inspectCheckpointRecovery(recoveryPath, expected) ++ if err != nil { ++ return false, err ++ } ++ return state == checkpointRecoveryCommitted, nil ++} ++ ++type checkpointRecoveryArtifact struct { ++ Path string `json:"path"` ++ Type string `json:"type"` ++ Mode uint32 `json:"mode"` ++ Size int64 `json:"size,omitempty"` ++ SHA256 string `json:"sha256,omitempty"` ++} ++ ++type checkpointRecoveryCommitRecord struct { ++ Version int `json:"version"` ++ Artifacts []checkpointRecoveryArtifact `json:"artifacts"` ++} ++ ++func checkpointRecoveryCommit(recoveryPath string) ([]byte, error) { ++ artifacts := make([]checkpointRecoveryArtifact, 0) ++ directories := []string{recoveryPath} ++ checkpointPaths := make([]string, 0, 1) ++ err := filepath.WalkDir(recoveryPath, func(path string, entry os.DirEntry, walkErr error) error { ++ if walkErr != nil { ++ return walkErr ++ } ++ relative, err := filepath.Rel(recoveryPath, path) + if err != nil { +- return fmt.Errorf("while acquiring eth0 in interior netns: %w", err) ++ return fmt.Errorf("while resolving recovery artifact path: %w", err) ++ } ++ if relative == "." { ++ return nil + } +- if err := netlink.LinkSetNsFd(eth0Link, int(podNetNS)); err != nil { +- return fmt.Errorf("while sending eth0 back to pod netns: %w", err) ++ relative = filepath.ToSlash(relative) ++ if relative == checkpointRecoveryCommitName { ++ return nil ++ } ++ ++ info, err := entry.Info() ++ if err != nil { ++ return fmt.Errorf("while stating recovery artifact %q: %w", relative, err) ++ } ++ mode := info.Mode() ++ if mode&os.ModeSymlink != 0 { ++ return fmt.Errorf("recovery artifact %q is a symlink", relative) ++ } ++ if mode.IsDir() { ++ directories = append(directories, path) ++ artifacts = append(artifacts, checkpointRecoveryArtifact{ ++ Path: relative, ++ Type: "directory", ++ Mode: uint32(mode.Perm()), ++ }) ++ return nil ++ } ++ if !mode.IsRegular() { ++ return fmt.Errorf("recovery artifact %q is not a regular file or directory", relative) ++ } ++ ++ file, err := os.Open(path) ++ if err != nil { ++ return fmt.Errorf("while opening recovery artifact %q: %w", relative, err) ++ } ++ if err := file.Sync(); err != nil { ++ _ = file.Close() ++ return fmt.Errorf("while syncing recovery artifact %q: %w", relative, err) ++ } ++ digest := sha256.New() ++ size, err := io.Copy(digest, file) ++ if err != nil { ++ _ = file.Close() ++ return fmt.Errorf("while hashing recovery artifact %q: %w", relative, err) ++ } ++ if err := file.Close(); err != nil { ++ return fmt.Errorf("while closing recovery artifact %q: %w", relative, err) ++ } ++ artifacts = append(artifacts, checkpointRecoveryArtifact{ ++ Path: relative, ++ Type: "file", ++ Mode: uint32(mode.Perm()), ++ Size: size, ++ SHA256: fmt.Sprintf("%x", digest.Sum(nil)), ++ }) ++ if relative != checkpointRecoveryManifestName { ++ checkpointPaths = append(checkpointPaths, relative) + } + return nil + }) + if err != nil { +- return nil, fmt.Errorf("while restoring eth0 in interior netns: %w", err) ++ return nil, fmt.Errorf("while inventorying recovery artifacts: %w", err) ++ } ++ sort.Strings(checkpointPaths) ++ if len(checkpointPaths) != 1 || checkpointPaths[0] != "checkpoint.img" { ++ return nil, fmt.Errorf( ++ "recovery checkpoint artifact set is %v, want exactly [checkpoint.img]", ++ checkpointPaths, ++ ) + } + +- s.actorLogger.EmitLifecycleLog("Actor checkpointed", req.GetActorId(), req.GetActorTemplateName(), req.GetActorTemplateNamespace()) ++ // Sync directory entries from the leaves up before publishing the commit ++ // record. A matching record therefore covers every regular file and directory ++ // that runsc returned, not only checkpoint.img. ++ sort.Slice(directories, func(i, j int) bool { ++ leftDepth := strings.Count(filepath.Clean(directories[i]), string(filepath.Separator)) ++ rightDepth := strings.Count(filepath.Clean(directories[j]), string(filepath.Separator)) ++ if leftDepth == rightDepth { ++ return directories[i] < directories[j] ++ } ++ return leftDepth > rightDepth ++ }) ++ for _, directory := range directories { ++ handle, err := os.Open(directory) ++ if err != nil { ++ return nil, fmt.Errorf("while opening recovery directory for sync: %w", err) ++ } ++ if err := handle.Sync(); err != nil { ++ _ = handle.Close() ++ return nil, fmt.Errorf("while syncing recovery directory: %w", err) ++ } ++ if err := handle.Close(); err != nil { ++ return nil, fmt.Errorf("while closing recovery directory: %w", err) ++ } ++ } + +- return nil, nil ++ sort.Slice(artifacts, func(i, j int) bool { ++ return artifacts[i].Path < artifacts[j].Path ++ }) ++ commit, err := json.Marshal(checkpointRecoveryCommitRecord{ ++ Version: 1, ++ Artifacts: artifacts, ++ }) ++ if err != nil { ++ return nil, fmt.Errorf("while encoding recovery commit: %w", err) ++ } ++ return append(commit, '\n'), nil ++} ++ ++func writeAtomicRecoveryFile(directory, temporaryDirectory, name, prefix string, data []byte) (retErr error) { ++ temporary, err := os.CreateTemp(temporaryDirectory, prefix) ++ if err != nil { ++ return fmt.Errorf("while creating temporary recovery file: %w", err) ++ } ++ temporaryPath := temporary.Name() ++ defer func() { ++ _ = temporary.Close() ++ if retErr != nil { ++ _ = os.Remove(temporaryPath) ++ } ++ }() ++ if err := temporary.Chmod(0o600); err != nil { ++ return fmt.Errorf("while setting temporary recovery file permissions: %w", err) ++ } ++ if _, err := temporary.Write(data); err != nil { ++ return fmt.Errorf("while writing temporary recovery file: %w", err) ++ } ++ if err := temporary.Sync(); err != nil { ++ return fmt.Errorf("while syncing temporary recovery file: %w", err) ++ } ++ if err := temporary.Close(); err != nil { ++ return fmt.Errorf("while closing temporary recovery file: %w", err) ++ } ++ if err := os.Rename(temporaryPath, filepath.Join(directory, name)); err != nil { ++ return fmt.Errorf("while committing recovery file: %w", err) ++ } ++ if err := syncDirectory(directory); err != nil { ++ return err ++ } ++ if filepath.Clean(temporaryDirectory) != filepath.Clean(directory) { ++ if err := syncDirectory(temporaryDirectory); err != nil { ++ return err ++ } ++ } ++ return nil ++} ++ ++func syncDirectory(directory string) error { ++ handle, err := os.Open(directory) ++ if err != nil { ++ return fmt.Errorf("while opening recovery directory for sync: %w", err) ++ } ++ defer handle.Close() ++ if err := handle.Sync(); err != nil { ++ return fmt.Errorf("while syncing recovery directory: %w", err) ++ } ++ return nil ++} ++ ++func prepareCheckpointRecovery(checkpointPath, recoveryPath string, expected map[string]struct{}) (retErr error) { ++ if _, err := os.Lstat(recoveryPath); err == nil { ++ return fmt.Errorf("refusing to replace existing recovery path %q", recoveryPath) ++ } else if !errors.Is(err, os.ErrNotExist) { ++ return fmt.Errorf("while checking recovery path: %w", err) ++ } ++ parent := filepath.Dir(recoveryPath) ++ if err := os.MkdirAll(parent, 0o700); err != nil { ++ return fmt.Errorf("while creating recovery parent directory: %w", err) ++ } ++ stagingPath, err := os.MkdirTemp(parent, checkpointRecoveryPreparePrefix) ++ if err != nil { ++ return fmt.Errorf("while creating recovery staging directory: %w", err) ++ } ++ published := false ++ defer func() { ++ if !published || retErr != nil { ++ _ = os.RemoveAll(stagingPath) ++ _ = os.RemoveAll(recoveryPath) ++ } ++ }() ++ if err := writeAtomicRecoveryFile( ++ stagingPath, ++ stagingPath, ++ checkpointRecoveryManifestName, ++ checkpointRecoveryPreparePrefix, ++ checkpointRecoveryManifest(expected), ++ ); err != nil { ++ return fmt.Errorf("while writing recovery manifest: %w", err) ++ } ++ if err := os.Rename(stagingPath, recoveryPath); err != nil { ++ return fmt.Errorf("while publishing prepared recovery directory: %w", err) ++ } ++ published = true ++ parentHandle, err := os.Open(parent) ++ if err != nil { ++ return fmt.Errorf("while opening recovery parent for sync: %w", err) ++ } ++ if err := parentHandle.Sync(); err != nil { ++ _ = parentHandle.Close() ++ return fmt.Errorf("while syncing recovery parent: %w", err) ++ } ++ if err := parentHandle.Close(); err != nil { ++ return fmt.Errorf("while closing recovery parent: %w", err) ++ } ++ if err := os.RemoveAll(checkpointPath); err != nil { ++ return fmt.Errorf("while clearing checkpoint path: %w", err) ++ } ++ if err := os.Symlink(recoveryPath, checkpointPath); err != nil { ++ return fmt.Errorf("while linking checkpoint path to recovery: %w", err) ++ } ++ return nil ++} ++ ++func commitCheckpointRecovery(recoveryPath string, expected map[string]struct{}) error { ++ state, err := inspectCheckpointRecovery(recoveryPath, expected) ++ if err != nil { ++ return err ++ } ++ if state != checkpointRecoveryPrepared { ++ return fmt.Errorf("recovery state is %d, want prepared", state) ++ } ++ commit, err := checkpointRecoveryCommit(recoveryPath) ++ if err != nil { ++ return err ++ } ++ if err := writeAtomicRecoveryFile( ++ recoveryPath, ++ filepath.Dir(recoveryPath), ++ checkpointRecoveryCommitName, ++ checkpointRecoveryCommitPrefix, ++ commit, ++ ); err != nil { ++ return err ++ } ++ state, err = inspectCheckpointRecovery(recoveryPath, expected) ++ if err != nil { ++ return err ++ } ++ if state != checkpointRecoveryCommitted { ++ return fmt.Errorf("recovery state is %d after commit, want committed", state) ++ } ++ return nil ++} ++ ++func resetPreparedCheckpointRecovery(checkpointPath, recoveryPath string, expected map[string]struct{}) error { ++ state, err := inspectCheckpointRecovery(recoveryPath, expected) ++ if err != nil { ++ return err ++ } ++ if state != checkpointRecoveryPrepared { ++ return fmt.Errorf("recovery state is %d, want prepared", state) ++ } ++ if err := os.RemoveAll(checkpointPath); err != nil { ++ return fmt.Errorf("while clearing ordinary checkpoint path: %w", err) ++ } ++ if err := os.RemoveAll(recoveryPath); err != nil { ++ return fmt.Errorf("while clearing prepared recovery path: %w", err) ++ } ++ return nil ++} ++ ++func restoreCheckpointRecovery(checkpointPath, recoveryPath string, expected map[string]struct{}) error { ++ valid, err := validateCheckpointRecovery(recoveryPath, expected) ++ if err != nil { ++ return err ++ } ++ if !valid { ++ return fmt.Errorf("checkpoint recovery is not committed") ++ } ++ return materializeCheckpointTransport(checkpointPath, recoveryPath) ++} ++ ++func materializeCheckpointTransport(checkpointPath, recoveryPath string) error { ++ if err := os.RemoveAll(checkpointPath); err != nil { ++ return fmt.Errorf("while clearing checkpoint transport path: %w", err) ++ } ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ return fmt.Errorf("while creating checkpoint transport path: %w", err) ++ } ++ if err := os.Link( ++ filepath.Join(recoveryPath, "checkpoint.img"), ++ filepath.Join(checkpointPath, "checkpoint.img"), ++ ); err != nil { ++ return fmt.Errorf("while linking committed checkpoint into transport view: %w", err) ++ } ++ compatibilityArtifacts := map[string]string{ ++ "pages.img": checkpointPagesCompatibilityMarker, ++ "pages_meta.img": checkpointPagesMetadataCompatibilityMarker, ++ } ++ for artifact, marker := range compatibilityArtifacts { ++ if err := writeAtomicRecoveryFile( ++ checkpointPath, ++ checkpointPath, ++ artifact, ++ ".checkpoint-transport-", ++ []byte(marker), ++ ); err != nil { ++ return fmt.Errorf("while creating checkpoint transport artifact %s: %w", artifact, err) ++ } ++ } ++ return nil ++} ++ ++func (s *AteomService) restorePodNetwork(ctx context.Context) error { ++ ops := podNetworkOps{ ++ podEth0Present: func(context.Context) (bool, error) { ++ return linkPresent("eth0") ++ }, ++ interiorEth0Present: func(ctx context.Context) (bool, error) { ++ var present bool ++ err := netNSDo(ctx, s.interiorNetNS, func(context.Context) error { ++ var err error ++ present, err = linkPresent("eth0") ++ return err ++ }) ++ return present, err ++ }, ++ moveInteriorEth0ToPod: func(ctx context.Context) error { ++ podNetNS, err := netns.Get() ++ if err != nil { ++ return fmt.Errorf("while getting pod netns: %w", err) ++ } ++ defer podNetNS.Close() ++ return netNSDo(ctx, s.interiorNetNS, func(context.Context) error { ++ eth0Link, err := netlink.LinkByName("eth0") ++ if err != nil { ++ return fmt.Errorf("while acquiring eth0 in interior netns: %w", err) ++ } ++ if err := netlink.LinkSetNsFd(eth0Link, int(podNetNS)); err != nil { ++ return fmt.Errorf("while sending eth0 back to pod netns: %w", err) ++ } ++ return nil ++ }) ++ }, ++ } ++ return ensurePodNetwork(ctx, ops) ++} ++ ++func ensurePodNetwork(ctx context.Context, ops podNetworkOps) error { ++ podPresent, interiorPresent, err := readPodNetworkState(ctx, ops) ++ if err != nil { ++ return err ++ } ++ switch { ++ case podPresent && !interiorPresent: ++ return nil ++ case !podPresent && interiorPresent: ++ if err := ops.moveInteriorEth0ToPod(ctx); err != nil { ++ return fmt.Errorf("while moving eth0 from interior to pod netns: %w", err) ++ } ++ podPresent, interiorPresent, err = readPodNetworkState(ctx, ops) ++ if err != nil { ++ return fmt.Errorf("failed closed while verifying restored eth0 location: %w", err) ++ } ++ if !podPresent || interiorPresent { ++ return fmt.Errorf( ++ "failed closed: restored eth0 location is uncertain (pod=%t interior=%t)", ++ podPresent, ++ interiorPresent, ++ ) ++ } ++ return nil ++ default: ++ return fmt.Errorf( ++ "failed closed: eth0 location is uncertain (pod=%t interior=%t)", ++ podPresent, ++ interiorPresent, ++ ) ++ } ++} ++ ++func readPodNetworkState(ctx context.Context, ops podNetworkOps) (bool, bool, error) { ++ podPresent, err := ops.podEth0Present(ctx) ++ if err != nil { ++ return false, false, fmt.Errorf("while checking eth0 in pod netns: %w", err) ++ } ++ interiorPresent, err := ops.interiorEth0Present(ctx) ++ if err != nil { ++ return false, false, fmt.Errorf("while checking eth0 in interior netns: %w", err) ++ } ++ return podPresent, interiorPresent, nil ++} ++ ++func linkPresent(name string) (bool, error) { ++ _, err := netlink.LinkByName(name) ++ if err == nil { ++ return true, nil ++ } ++ var notFound netlink.LinkNotFoundError ++ if errors.As(err, ¬Found) { ++ return false, nil ++ } ++ return false, err ++} ++ ++func prepareCheckpointRestore(checkpointDir string) error { ++ type compatibilityArtifact struct { ++ name string ++ marker string ++ } ++ artifacts := []compatibilityArtifact{ ++ {name: "pages.img", marker: checkpointPagesCompatibilityMarker}, ++ {name: "pages_meta.img", marker: checkpointPagesMetadataCompatibilityMarker}, ++ } ++ present := make([]bool, len(artifacts)) ++ compatibility := make([]bool, len(artifacts)) ++ for index, artifact := range artifacts { ++ path := filepath.Join(checkpointDir, artifact.name) ++ info, err := os.Lstat(path) ++ if errors.Is(err, os.ErrNotExist) { ++ continue ++ } ++ if err != nil { ++ return fmt.Errorf("while stating %s: %w", artifact.name, err) ++ } ++ if !info.Mode().IsRegular() { ++ return fmt.Errorf("checkpoint artifact %s must be a regular file", artifact.name) ++ } ++ present[index] = true ++ if info.Size() == int64(len(artifact.marker)) { ++ content, err := os.ReadFile(path) ++ if err != nil { ++ return fmt.Errorf("while reading %s: %w", artifact.name, err) ++ } ++ compatibility[index] = string(content) == artifact.marker ++ } ++ } ++ ++ anyCompatibility := compatibility[0] || compatibility[1] ++ if anyCompatibility { ++ for index, artifact := range artifacts { ++ if present[index] && !compatibility[index] { ++ return fmt.Errorf("checkpoint mixes compatibility and native page artifacts") ++ } ++ if !present[index] { ++ continue ++ } ++ if err := os.Remove(filepath.Join(checkpointDir, artifact.name)); err != nil { ++ return fmt.Errorf("while removing %s compatibility file: %w", artifact.name, err) ++ } ++ } ++ return nil ++ } ++ ++ if present[0] != present[1] { ++ return fmt.Errorf("native checkpoint has only one of pages.img/pages_meta.img") ++ } ++ // Both absent is a native compressed snapshot. Both present is a native ++ // uncompressed snapshot, even when one file is legitimately empty. ++ return nil + } + + func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.RestoreWorkloadRequest) (*ateompb.RestoreWorkloadResponse, error) { +@@ -336,6 +1118,11 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore + // * All OCI bundles are set up, including for "pause" container. + // * Checkpoint downloaded and placed on disk + ++ checkpointDir := ateompath.CheckpointDir(req.GetActorTemplateNamespace(), req.GetActorTemplateName(), req.GetActorId()) ++ if err := prepareCheckpointRestore(checkpointDir); err != nil { ++ return nil, fmt.Errorf("while preparing checkpoint files for restore: %w", err) ++ } ++ + // Move pod eth0 into interior netns + eth0Link, err := netlink.LinkByName("eth0") + if err != nil { +@@ -384,8 +1171,6 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore + actorID: req.GetActorId(), + } + +- checkpointDir := ateompath.CheckpointDir(req.GetActorTemplateNamespace(), req.GetActorTemplateName(), req.GetActorId()) +- + // Create and restore pause container + if err := rcmd.cmdCreate(ctx, os.Stdout, "pause"); err != nil { + return nil, fmt.Errorf("while creating pause container: %w", err) +diff --git a/cmd/servers/ateom-gvisor/runsc.go b/cmd/servers/ateom-gvisor/runsc.go +index 6db499a5..00763bfc 100644 +--- a/cmd/servers/ateom-gvisor/runsc.go ++++ b/cmd/servers/ateom-gvisor/runsc.go +@@ -15,12 +15,18 @@ + package main + + import ( ++ "bufio" ++ "bytes" + "context" ++ "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "os/exec" ++ "path/filepath" ++ "strings" ++ "time" + + "github.com/agent-substrate/substrate/internal/ateompath" + ) +@@ -116,6 +122,7 @@ func (r *runsc) cmdCheckpoint(ctx context.Context, containerName, checkpointPath + "-root", ateompath.RunSCStateDir(r.actorTemplateNamespace, r.actorTemplateName, r.actorID), + "checkpoint", + "-image-path", checkpointPath, ++ "-compression=flate-best-speed", + containerName, // Name of the container + ) + cmd.Stdout = os.Stdout +@@ -127,6 +134,64 @@ func (r *runsc) cmdCheckpoint(ctx context.Context, containerName, checkpointPath + return nil + } + ++func (r *runsc) cmdValidateCheckpoint(ctx context.Context, checkpointPath string) error { ++ reapLock.RLock() ++ defer reapLock.RUnlock() ++ ++ slog.InfoContext(ctx, "About to validate runsc checkpoint") ++ cmd := exec.CommandContext( ++ ctx, ++ r.path, ++ "-log-format", "json", ++ "--alsologtostderr", ++ "-root", ateompath.RunSCStateDir(r.actorTemplateNamespace, r.actorTemplateName, r.actorID), ++ "statefile", ++ filepath.Join(checkpointPath, "checkpoint.img"), ++ ) ++ cmd.Stdout = io.Discard ++ cmd.Stderr = io.Discard ++ if err := cmd.Run(); err != nil { ++ return fmt.Errorf("while validating runsc checkpoint statefile: %w", err) ++ } ++ return nil ++} ++ ++func (r *runsc) containerStatus(ctx context.Context, containerName string) (string, error) { ++ reapLock.RLock() ++ defer reapLock.RUnlock() ++ ++ var stdout bytes.Buffer ++ var stderr bytes.Buffer ++ cmd := exec.CommandContext( ++ ctx, ++ r.path, ++ "-log-format", "json", ++ "--alsologtostderr", ++ "-root", ateompath.RunSCStateDir(r.actorTemplateNamespace, r.actorTemplateName, r.actorID), ++ "state", ++ containerName, ++ ) ++ cmd.Stdout = &stdout ++ cmd.Stderr = &stderr ++ if err := cmd.Run(); err != nil { ++ detail := strings.TrimSpace(stderr.String()) ++ if detail != "" { ++ return "", fmt.Errorf("while reading runsc container state: %w: %s", err, detail) ++ } ++ return "", fmt.Errorf("while reading runsc container state: %w", err) ++ } ++ var state struct { ++ Status string `json:"status"` ++ } ++ if err := json.Unmarshal(stdout.Bytes(), &state); err != nil { ++ return "", fmt.Errorf("while parsing runsc container state: %w", err) ++ } ++ if state.Status == "" { ++ return "", fmt.Errorf("runsc container state omitted status") ++ } ++ return strings.ToLower(state.Status), nil ++} ++ + // We take a checkpoint only of the root container of the sandbox, but we need + // to call restore on each container, using the same checkpoint. + func (r *runsc) cmdRestore(ctx context.Context, out io.Writer, containerName, checkpointPath string) error { +@@ -163,33 +228,140 @@ func (r *runsc) cmdRestore(ctx context.Context, out io.Writer, containerName, ch + return nil + } + ++const ( ++ runscDeleteAttempts = 4 ++ runscDeleteRetryDelay = 100 * time.Millisecond ++) ++ + func (r *runsc) cmdDelete(ctx context.Context, containerName string) error { + reapLock.RLock() + defer reapLock.RUnlock() + +- // token := rand.Text() +- // logFile := "/tmp/runsc.delete." + token + ".log" ++ var lastErr error ++ for attempt := 1; attempt <= runscDeleteAttempts; attempt++ { ++ var output bytes.Buffer ++ cmd := exec.CommandContext( ++ ctx, ++ r.path, ++ "-log-format", "json", ++ "--alsologtostderr", ++ // "-debug", ++ "-root", ateompath.RunSCStateDir(r.actorTemplateNamespace, r.actorTemplateName, r.actorID), ++ "delete", ++ "-force", ++ containerName, ++ ) ++ cmd.Stdout = &output ++ cmd.Stderr = &output ++ ++ runErr := cmd.Run() ++ if runErr != nil { ++ lastErr = fmt.Errorf("while running `runsc delete`: %w", runErr) ++ if detail := strings.TrimSpace(output.String()); detail != "" { ++ lastErr = fmt.Errorf("%w: %s", lastErr, detail) ++ } ++ } else { ++ lastErr = nil ++ } ++ ++ present, err := r.containerPresentLocked(ctx, containerName) ++ if err != nil { ++ outcome := "runsc delete exited successfully" ++ if lastErr != nil { ++ outcome = lastErr.Error() ++ } ++ return fmt.Errorf("%s; failed closed while verifying container absence: %w", outcome, err) ++ } ++ if !present { ++ if lastErr != nil { ++ slog.WarnContext(ctx, "runsc delete returned an error after removing the container; accepting verified absence", ++ slog.String("container", containerName), ++ slog.Int("attempt", attempt), ++ slog.Any("err", lastErr)) ++ } ++ return nil ++ } ++ if lastErr == nil { ++ lastErr = fmt.Errorf("runsc delete exited successfully but container remains present") ++ } ++ if attempt == runscDeleteAttempts { ++ break ++ } ++ ++ slog.WarnContext(ctx, "runsc delete did not remove the container; retrying", ++ slog.String("container", containerName), ++ slog.Int("attempt", attempt), ++ slog.Any("err", lastErr)) ++ timer := time.NewTimer(runscDeleteRetryDelay * time.Duration(attempt)) ++ select { ++ case <-ctx.Done(): ++ if !timer.Stop() { ++ select { ++ case <-timer.C: ++ default: ++ } ++ } ++ return fmt.Errorf("%w; delete retry canceled: %v", lastErr, ctx.Err()) ++ case <-timer.C: ++ } ++ } ++ ++ return fmt.Errorf("%w after %d attempts; container is still present", lastErr, runscDeleteAttempts) ++} ++ ++func (r *runsc) containerPresentLocked(ctx context.Context, containerName string) (bool, error) { ++ containers, err := r.containerNamesLocked(ctx) ++ if err != nil { ++ return false, err ++ } ++ _, present := containers[containerName] ++ return present, nil ++} + ++func (r *runsc) containerNames(ctx context.Context) (map[string]struct{}, error) { ++ reapLock.RLock() ++ defer reapLock.RUnlock() ++ return r.containerNamesLocked(ctx) ++} ++ ++func (r *runsc) containerNamesLocked(ctx context.Context) (map[string]struct{}, error) { ++ var stdout bytes.Buffer ++ var stderr bytes.Buffer + cmd := exec.CommandContext( + ctx, + r.path, + "-log-format", "json", + "--alsologtostderr", +- // "-debug", + "-root", ateompath.RunSCStateDir(r.actorTemplateNamespace, r.actorTemplateName, r.actorID), +- "delete", +- "-force", +- containerName, ++ "list", ++ "--quiet", + ) +- cmd.Stdout = os.Stdout +- cmd.Stderr = os.Stderr +- +- err := cmd.Run() +- if err != nil { +- return fmt.Errorf("while running `runsc delete`: %w", err) ++ cmd.Stdout = &stdout ++ cmd.Stderr = &stderr ++ if err := cmd.Run(); err != nil { ++ detail := strings.TrimSpace(stderr.String()) ++ if detail != "" { ++ return nil, fmt.Errorf("while running `runsc list --quiet`: %w: %s", err, detail) ++ } ++ return nil, fmt.Errorf("while running `runsc list --quiet`: %w", err) + } + +- return nil ++ containers := make(map[string]struct{}) ++ scanner := bufio.NewScanner(&stdout) ++ for scanner.Scan() { ++ containerName := strings.TrimSpace(scanner.Text()) ++ if containerName == "" { ++ continue ++ } ++ if _, duplicate := containers[containerName]; duplicate { ++ return nil, fmt.Errorf("duplicate container %q in `runsc list --quiet` output", containerName) ++ } ++ containers[containerName] = struct{}{} ++ } ++ if err := scanner.Err(); err != nil { ++ return nil, fmt.Errorf("while reading `runsc list --quiet` output: %w", err) ++ } ++ return containers, nil + } + + func (r *runsc) cmdState(ctx context.Context, containerName string) error { +diff --git a/cmd/servers/ateom-gvisor/runsc_test.go b/cmd/servers/ateom-gvisor/runsc_test.go +new file mode 100644 +index 00000000..187aee4f +--- /dev/null ++++ b/cmd/servers/ateom-gvisor/runsc_test.go +@@ -0,0 +1,855 @@ ++// Copyright 2026 Google LLC ++// ++// Licensed under the Apache License, Version 2.0 (the "License"); ++// you may not use this file except in compliance with the License. ++// You may obtain a copy of the License at ++// ++// http://www.apache.org/licenses/LICENSE-2.0 ++// ++// Unless required by applicable law or agreed to in writing, software ++// distributed under the License is distributed on an "AS IS" BASIS, ++// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++// See the License for the specific language governing permissions and ++// limitations under the License. ++ ++package main ++ ++import ( ++ "context" ++ "errors" ++ "os" ++ "path/filepath" ++ "slices" ++ "strconv" ++ "strings" ++ "testing" ++ "time" ++) ++ ++func writeFakeRunsc(t *testing.T, mode string, presentContainers ...string) (string, string) { ++ t.Helper() ++ ++ stateDir := t.TempDir() ++ if err := os.WriteFile(filepath.Join(stateDir, "mode"), []byte(mode), 0o600); err != nil { ++ t.Fatalf("write mode: %v", err) ++ } ++ if len(presentContainers) > 0 { ++ present := strings.Join(presentContainers, "\n") + "\n" ++ if err := os.WriteFile(filepath.Join(stateDir, "present"), []byte(present), 0o600); err != nil { ++ t.Fatalf("write present state: %v", err) ++ } ++ } ++ if err := os.WriteFile(filepath.Join(stateDir, "status"), []byte("running\n"), 0o600); err != nil { ++ t.Fatalf("write status: %v", err) ++ } ++ ++ script := filepath.Join(stateDir, "runsc") ++ const body = `#!/bin/sh ++set -eu ++state_dir="${RUNSC_FAKE_STATE:?}" ++mode="$(cat "$state_dir/mode")" ++command="" ++image_path="" ++previous="" ++last="" ++for arg in "$@"; do ++ if [ "$previous" = "-image-path" ]; then ++ image_path="$arg" ++ fi ++ case "$arg" in ++ checkpoint|delete|list|state|statefile) ++ command="$arg" ++ ;; ++ esac ++ previous="$arg" ++ last="$arg" ++done ++container="$last" ++ ++container_present() { ++ [ -f "$state_dir/present" ] && grep -Fxq "$1" "$state_dir/present" ++} ++ ++remove_container() { ++ temporary="$state_dir/present.tmp" ++ if [ -f "$state_dir/present" ]; then ++ grep -Fxv "$1" "$state_dir/present" > "$temporary" || true ++ mv "$temporary" "$state_dir/present" ++ fi ++} ++ ++increment_counter() { ++ counter_file="$state_dir/$1" ++ count=0 ++ if [ -f "$counter_file" ]; then ++ count="$(cat "$counter_file")" ++ fi ++ count=$((count + 1)) ++ printf '%s' "$count" > "$counter_file" ++ printf '%s' "$count" ++} ++ ++case "$command" in ++ checkpoint) ++ count="$(increment_counter checkpoint-count)" ++ printf '%s\n' "$@" > "$state_dir/checkpoint-args" ++ mkdir -p "$image_path" ++ if [ "$mode" = "checkpoint-partial-once" ] && [ "$count" -eq 1 ]; then ++ printf 'partial-snapshot\n' > "$image_path/checkpoint.img" ++ printf 'running\n' > "$state_dir/status" ++ printf 'simulated interrupted checkpoint\n' >&2 ++ exit 74 ++ fi ++ printf 'snapshot-%s\n' "$count" > "$image_path/checkpoint.img" ++ printf 'stopped\n' > "$state_dir/status" ++ ;; ++ delete) ++ count="$(increment_counter delete-count-$container)" ++ case "$mode" in ++ absent-after-error) ++ remove_container "$container" ++ printf 'simulated delete exit 128\n' >&2 ++ exit 128 ++ ;; ++ retry-then-success) ++ if [ "$count" -eq 1 ]; then ++ printf 'simulated transient delete exit 128\n' >&2 ++ exit 128 ++ fi ++ remove_container "$container" ++ ;; ++ persistent-error|list-error) ++ printf 'simulated persistent delete exit 128\n' >&2 ++ exit 128 ++ ;; ++ two-call-recovery) ++ if [ "$container" = "app-b" ] && [ "$count" -le 4 ]; then ++ printf 'simulated persistent app-b delete exit 128\n' >&2 ++ exit 128 ++ fi ++ remove_container "$container" ++ ;; ++ success|checkpoint-partial-once|state-warning) ++ remove_container "$container" ++ ;; ++ success-but-present) ++ ;; ++ *) ++ printf 'unexpected fake runsc mode %s\n' "$mode" >&2 ++ exit 65 ++ ;; ++ esac ++ ;; ++ list) ++ if [ "$mode" = "list-error" ]; then ++ printf 'simulated list failure\n' >&2 ++ exit 2 ++ fi ++ if [ -f "$state_dir/present" ]; then ++ cat "$state_dir/present" ++ fi ++ ;; ++ state) ++ if ! container_present "$container"; then ++ printf 'container %s is absent\n' "$container" >&2 ++ exit 3 ++ fi ++ if [ "$mode" = "state-warning" ]; then ++ printf 'simulated runsc state warning\n' >&2 ++ fi ++ status="$(cat "$state_dir/status")" ++ printf '{"id":"%s","status":"%s"}\n' "$container" "$status" ++ ;; ++ statefile) ++ if ! grep -Eq '^snapshot-[0-9]+$' "$container"; then ++ printf 'simulated invalid statefile\n' >&2 ++ exit 75 ++ fi ++ printf 'valid statefile\n' ++ ;; ++ *) ++ printf 'unexpected fake runsc command\n' >&2 ++ exit 64 ++ ;; ++esac ++` ++ if err := os.WriteFile(script, []byte(body), 0o700); err != nil { ++ t.Fatalf("write fake runsc: %v", err) ++ } ++ t.Setenv("RUNSC_FAKE_STATE", stateDir) ++ return script, stateDir ++} ++ ++func testRunsc(path string) *runsc { ++ return &runsc{ ++ path: path, ++ actorTemplateNamespace: "test-namespace", ++ actorTemplateName: "test-template", ++ actorID: "test-actor", ++ } ++} ++ ++func counterValue(t *testing.T, stateDir, name string) int { ++ t.Helper() ++ data, err := os.ReadFile(filepath.Join(stateDir, name)) ++ if os.IsNotExist(err) { ++ return 0 ++ } ++ if err != nil { ++ t.Fatalf("read counter %s: %v", name, err) ++ } ++ count, err := strconv.Atoi(string(data)) ++ if err != nil { ++ t.Fatalf("parse counter %s: %v", name, err) ++ } ++ return count ++} ++ ++func presentContainerSet(t *testing.T, stateDir string) map[string]struct{} { ++ t.Helper() ++ data, err := os.ReadFile(filepath.Join(stateDir, "present")) ++ if os.IsNotExist(err) { ++ return map[string]struct{}{} ++ } ++ if err != nil { ++ t.Fatalf("read present containers: %v", err) ++ } ++ containers := make(map[string]struct{}) ++ for _, name := range strings.Fields(string(data)) { ++ containers[name] = struct{}{} ++ } ++ return containers ++} ++ ++func TestCmdDeleteAcceptsVerifiedAbsenceAfterError(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "absent-after-error", "workspace") ++ if err := testRunsc(path).cmdDelete(context.Background(), "workspace"); err != nil { ++ t.Fatalf("cmdDelete() error = %v, want verified absence success", err) ++ } ++ if got := counterValue(t, stateDir, "delete-count-workspace"); got != 1 { ++ t.Fatalf("delete attempts = %d, want 1", got) ++ } ++} ++ ++func TestCmdDeleteRetriesWhileContainerRemains(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "retry-then-success", "workspace") ++ if err := testRunsc(path).cmdDelete(context.Background(), "workspace"); err != nil { ++ t.Fatalf("cmdDelete() error = %v, want retry success", err) ++ } ++ if got := counterValue(t, stateDir, "delete-count-workspace"); got != 2 { ++ t.Fatalf("delete attempts = %d, want 2", got) ++ } ++} ++ ++func TestCmdDeleteFailsClosedWhenContainerRemains(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "persistent-error", "workspace") ++ err := testRunsc(path).cmdDelete(context.Background(), "workspace") ++ if err == nil { ++ t.Fatal("cmdDelete() error = nil, want persistent failure") ++ } ++ if !strings.Contains(err.Error(), "container is still present") { ++ t.Fatalf("cmdDelete() error = %q, want presence postcondition", err) ++ } ++ if got := counterValue(t, stateDir, "delete-count-workspace"); got != runscDeleteAttempts { ++ t.Fatalf("delete attempts = %d, want %d", got, runscDeleteAttempts) ++ } ++} ++ ++func TestCmdDeleteFailsClosedWhenSuccessLeavesContainerPresent(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "success-but-present", "workspace") ++ err := testRunsc(path).cmdDelete(context.Background(), "workspace") ++ if err == nil { ++ t.Fatal("cmdDelete() error = nil, want verified-presence failure") ++ } ++ if !strings.Contains(err.Error(), "exited successfully but container remains present") { ++ t.Fatalf("cmdDelete() error = %q, want successful-exit presence failure", err) ++ } ++ if got := counterValue(t, stateDir, "delete-count-workspace"); got != runscDeleteAttempts { ++ t.Fatalf("delete attempts = %d, want %d", got, runscDeleteAttempts) ++ } ++} ++ ++func TestCmdDeleteFailsClosedWhenAbsenceCannotBeVerified(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "list-error", "workspace") ++ err := testRunsc(path).cmdDelete(context.Background(), "workspace") ++ if err == nil { ++ t.Fatal("cmdDelete() error = nil, want verification failure") ++ } ++ if !strings.Contains(err.Error(), "failed closed while verifying container absence") { ++ t.Fatalf("cmdDelete() error = %q, want verification failure context", err) ++ } ++ if got := counterValue(t, stateDir, "delete-count-workspace"); got != 1 { ++ t.Fatalf("delete attempts = %d, want 1", got) ++ } ++} ++ ++func TestContainerNamesWaitsForReaperReadLock(t *testing.T) { ++ path, _ := writeFakeRunsc(t, "success", "pause") ++ rcmd := testRunsc(path) ++ reapLock.Lock() ++ locked := true ++ defer func() { ++ if locked { ++ reapLock.Unlock() ++ } ++ }() ++ result := make(chan error, 1) ++ go func() { ++ _, err := rcmd.containerNames(context.Background()) ++ result <- err ++ }() ++ select { ++ case err := <-result: ++ t.Fatalf("containerNames() returned before reaper lock release: %v", err) ++ case <-time.After(50 * time.Millisecond): ++ } ++ reapLock.Unlock() ++ locked = false ++ select { ++ case err := <-result: ++ if err != nil { ++ t.Fatalf("containerNames() error = %v", err) ++ } ++ case <-time.After(2 * time.Second): ++ t.Fatal("containerNames() remained blocked after reaper lock release") ++ } ++} ++ ++func TestContainerStatusIgnoresStderrDiagnostics(t *testing.T) { ++ path, _ := writeFakeRunsc(t, "state-warning", "pause") ++ status, err := testRunsc(path).containerStatus(context.Background(), "pause") ++ if err != nil { ++ t.Fatalf("containerStatus() error = %v", err) ++ } ++ if status != "running" { ++ t.Fatalf("containerStatus() = %q, want running", status) ++ } ++} ++ ++func TestCmdCheckpointUsesStoppedSingleFileProtocol(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "success", "pause") ++ rcmd := testRunsc(path) ++ checkpointPath := filepath.Join(t.TempDir(), "checkpoint") ++ if err := rcmd.cmdCheckpoint(context.Background(), "pause", checkpointPath); err != nil { ++ t.Fatalf("cmdCheckpoint() error = %v", err) ++ } ++ args, err := os.ReadFile(filepath.Join(stateDir, "checkpoint-args")) ++ if err != nil { ++ t.Fatalf("read checkpoint args: %v", err) ++ } ++ fields := strings.Fields(string(args)) ++ if !slices.Contains(fields, "-compression=flate-best-speed") { ++ t.Fatalf("checkpoint args = %q, want pinned single-file compression", string(args)) ++ } ++ if slices.Contains(fields, "-leave-running") { ++ t.Fatalf("checkpoint args = %q, must keep the sandbox stopped", string(args)) ++ } ++ status, err := rcmd.containerStatus(context.Background(), "pause") ++ if err != nil { ++ t.Fatalf("containerStatus() error = %v", err) ++ } ++ if status != "stopped" { ++ t.Fatalf("checkpointed status = %q, want stopped", status) ++ } ++ if err := rcmd.cmdValidateCheckpoint(context.Background(), checkpointPath); err != nil { ++ t.Fatalf("cmdValidateCheckpoint() error = %v", err) ++ } ++} ++ ++func TestCheckpointRecoveryRejectsUnexpectedOrCorruptArtifacts(t *testing.T) { ++ path, _ := writeFakeRunsc(t, "success", "pause", "app") ++ rcmd := testRunsc(path) ++ checkpointPath := filepath.Join(t.TempDir(), "checkpoint") ++ recoveryPath := filepath.Join(t.TempDir(), checkpointRecoveryDirName) ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("create checkpoint directory: %v", err) ++ } ++ _, expected, err := expectedCheckpointContainers([]string{"app"}) ++ if err != nil { ++ t.Fatalf("expectedCheckpointContainers() error = %v", err) ++ } ++ if err := prepareCheckpointRecovery(checkpointPath, recoveryPath, expected); err != nil { ++ t.Fatalf("prepareCheckpointRecovery() error = %v", err) ++ } ++ if err := rcmd.cmdCheckpoint(context.Background(), "pause", checkpointPath); err != nil { ++ t.Fatalf("cmdCheckpoint() error = %v", err) ++ } ++ if err := rcmd.cmdValidateCheckpoint(context.Background(), recoveryPath); err != nil { ++ t.Fatalf("cmdValidateCheckpoint() error = %v", err) ++ } ++ ++ extraPath := filepath.Join(recoveryPath, "unexpected.img") ++ if err := os.WriteFile(extraPath, []byte("unexpected\n"), 0o600); err != nil { ++ t.Fatalf("write unexpected checkpoint artifact: %v", err) ++ } ++ if err := commitCheckpointRecovery(recoveryPath, expected); err == nil || ++ !strings.Contains(err.Error(), "want exactly [checkpoint.img]") { ++ t.Fatalf("commit with unexpected artifact error = %v, want single-file recovery rejection", err) ++ } ++ if err := os.Remove(extraPath); err != nil { ++ t.Fatalf("remove unexpected checkpoint artifact: %v", err) ++ } ++ if err := commitCheckpointRecovery(recoveryPath, expected); err != nil { ++ t.Fatalf("commitCheckpointRecovery() error = %v", err) ++ } ++ if valid, err := validateCheckpointRecovery(recoveryPath, expected); err != nil || !valid { ++ t.Fatalf("committed recovery = valid:%t error:%v, want valid", valid, err) ++ } ++ if err := materializeCheckpointTransport(checkpointPath, recoveryPath); err != nil { ++ t.Fatalf("materializeCheckpointTransport() error = %v", err) ++ } ++ recoveryInfo, err := os.Stat(filepath.Join(recoveryPath, "checkpoint.img")) ++ if err != nil { ++ t.Fatalf("stat recovery checkpoint: %v", err) ++ } ++ transportInfo, err := os.Stat(filepath.Join(checkpointPath, "checkpoint.img")) ++ if err != nil { ++ t.Fatalf("stat transport checkpoint: %v", err) ++ } ++ if !os.SameFile(recoveryInfo, transportInfo) { ++ t.Fatal("transport checkpoint is not linked to committed recovery") ++ } ++ expectedMarkers := map[string]string{ ++ "pages.img": checkpointPagesCompatibilityMarker, ++ "pages_meta.img": checkpointPagesMetadataCompatibilityMarker, ++ } ++ for artifact, marker := range expectedMarkers { ++ content, err := os.ReadFile(filepath.Join(checkpointPath, artifact)) ++ if err != nil { ++ t.Fatalf("read transport artifact %s: %v", artifact, err) ++ } ++ if string(content) != marker { ++ t.Fatalf("transport artifact %s = %q, want compatibility marker", artifact, content) ++ } ++ if _, err := os.Stat(filepath.Join(recoveryPath, artifact)); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("recovery unexpectedly contains transport artifact %s: %v", artifact, err) ++ } ++ } ++ ++ checkpointPath = filepath.Join(recoveryPath, "checkpoint.img") ++ if err := os.WriteFile(checkpointPath, []byte("truncated\n"), 0o600); err != nil { ++ t.Fatalf("corrupt checkpoint statefile: %v", err) ++ } ++ if valid, err := validateCheckpointRecovery(recoveryPath, expected); err == nil || valid || ++ !strings.Contains(err.Error(), "integrity") { ++ t.Fatalf("corrupted recovery = valid:%t error:%v, want integrity failure", valid, err) ++ } ++} ++ ++func TestCheckpointRecoveryReconcilesPreparationBeforeCheckpoint(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "success", "pause", "app") ++ rcmd := testRunsc(path) ++ checkpointPath := filepath.Join(t.TempDir(), "checkpoint") ++ recoveryPath := filepath.Join(t.TempDir(), checkpointRecoveryDirName) ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("create checkpoint directory: %v", err) ++ } ++ _, expected, err := expectedCheckpointContainers([]string{"app"}) ++ if err != nil { ++ t.Fatalf("expectedCheckpointContainers() error = %v", err) ++ } ++ if err := prepareCheckpointRecovery(checkpointPath, recoveryPath, expected); err != nil { ++ t.Fatalf("prepareCheckpointRecovery() error = %v", err) ++ } ++ ++ // Simulate a crash after durable preparation but before runsc starts, then ++ // atelet clearing and recreating only the ordinary checkpoint path. ++ if err := os.RemoveAll(checkpointPath); err != nil { ++ t.Fatalf("clear ordinary checkpoint path: %v", err) ++ } ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("recreate ordinary checkpoint path: %v", err) ++ } ++ if err := checkpointAndCleanup( ++ context.Background(), ++ rcmd, ++ []string{"app"}, ++ checkpointPath, ++ recoveryPath, ++ func(context.Context) error { return nil }, ++ ); err != nil { ++ t.Fatalf("checkpointAndCleanup() preparation recovery error = %v", err) ++ } ++ if got := counterValue(t, stateDir, "checkpoint-count"); got != 1 { ++ t.Fatalf("checkpoint attempts = %d, want one fresh checkpoint after preparation recovery", got) ++ } ++ if valid, err := validateCheckpointRecovery(recoveryPath, expected); err != nil || !valid { ++ t.Fatalf("checkpoint recovery after preparation retry = valid:%t error:%v, want committed", valid, err) ++ } ++} ++ ++func TestCheckpointRecoveryReconcilesUncommittedSuccessfulCheckpoint(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "success", "pause", "app") ++ rcmd := testRunsc(path) ++ checkpointPath := filepath.Join(t.TempDir(), "checkpoint") ++ recoveryPath := filepath.Join(t.TempDir(), checkpointRecoveryDirName) ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("create checkpoint directory: %v", err) ++ } ++ _, expected, err := expectedCheckpointContainers([]string{"app"}) ++ if err != nil { ++ t.Fatalf("expectedCheckpointContainers() error = %v", err) ++ } ++ ++ // Simulate a crash after runsc returned success but before the service could ++ // atomically write the digest-backed commit record. The sandbox is stopped, ++ // so the retry must validate the runsc statefile and commit the existing ++ // snapshot rather than attempting an unsafe second checkpoint. ++ if err := prepareCheckpointRecovery(checkpointPath, recoveryPath, expected); err != nil { ++ t.Fatalf("prepareCheckpointRecovery() error = %v", err) ++ } ++ if err := rcmd.cmdCheckpoint(context.Background(), "pause", checkpointPath); err != nil { ++ t.Fatalf("cmdCheckpoint() error = %v", err) ++ } ++ if state, err := inspectCheckpointRecovery(recoveryPath, expected); err != nil || state != checkpointRecoveryPrepared { ++ t.Fatalf("recovery before commit = state:%d error:%v, want prepared", state, err) ++ } ++ if err := os.RemoveAll(checkpointPath); err != nil { ++ t.Fatalf("clear ordinary checkpoint path: %v", err) ++ } ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("recreate ordinary checkpoint path: %v", err) ++ } ++ if err := checkpointAndCleanup( ++ context.Background(), ++ rcmd, ++ []string{"app"}, ++ checkpointPath, ++ recoveryPath, ++ func(context.Context) error { return nil }, ++ ); err != nil { ++ t.Fatalf("checkpointAndCleanup() uncommitted recovery error = %v", err) ++ } ++ if got := counterValue(t, stateDir, "checkpoint-count"); got != 1 { ++ t.Fatalf("checkpoint attempts = %d, want validated uncommitted snapshot reuse", got) ++ } ++ checkpoint, err := os.ReadFile(filepath.Join(checkpointPath, "checkpoint.img")) ++ if err != nil { ++ t.Fatalf("read recovered checkpoint: %v", err) ++ } ++ if got, want := string(checkpoint), "snapshot-1\n"; got != want { ++ t.Fatalf("recovered checkpoint = %q, want %q", got, want) ++ } ++ if valid, err := validateCheckpointRecovery(recoveryPath, expected); err != nil || !valid { ++ t.Fatalf("checkpoint recovery after uncommitted retry = valid:%t error:%v, want committed", valid, err) ++ } ++} ++ ++func TestCheckpointRecoveryReconcilesInterruptedWrite(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "checkpoint-partial-once", "pause", "app") ++ rcmd := testRunsc(path) ++ checkpointPath := filepath.Join(t.TempDir(), "checkpoint") ++ recoveryPath := filepath.Join(t.TempDir(), checkpointRecoveryDirName) ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("create checkpoint directory: %v", err) ++ } ++ networkRestores := 0 ++ restoreNetwork := func(context.Context) error { ++ networkRestores++ ++ return nil ++ } ++ ++ err := checkpointAndCleanup( ++ context.Background(), ++ rcmd, ++ []string{"app"}, ++ checkpointPath, ++ recoveryPath, ++ restoreNetwork, ++ ) ++ if err == nil || !strings.Contains(err.Error(), "exit status 74") { ++ t.Fatalf("first checkpointAndCleanup() error = %v, want interrupted checkpoint exit", err) ++ } ++ if networkRestores != 0 { ++ t.Fatalf("network restores after interrupted checkpoint = %d, want 0", networkRestores) ++ } ++ if got, want := formatContainerSet(presentContainerSet(t, stateDir)), "app,pause"; got != want { ++ t.Fatalf("present containers after interrupted checkpoint = %q, want %q", got, want) ++ } ++ _, expected, err := expectedCheckpointContainers([]string{"app"}) ++ if err != nil { ++ t.Fatalf("expectedCheckpointContainers() error = %v", err) ++ } ++ if state, err := inspectCheckpointRecovery(recoveryPath, expected); err != nil || state != checkpointRecoveryPrepared { ++ t.Fatalf("interrupted recovery = state:%d error:%v, want prepared", state, err) ++ } ++ ++ if err := os.RemoveAll(checkpointPath); err != nil { ++ t.Fatalf("clear ordinary checkpoint path: %v", err) ++ } ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("recreate ordinary checkpoint path: %v", err) ++ } ++ if err := checkpointAndCleanup( ++ context.Background(), ++ rcmd, ++ []string{"app"}, ++ checkpointPath, ++ recoveryPath, ++ restoreNetwork, ++ ); err != nil { ++ t.Fatalf("second checkpointAndCleanup() error = %v, want fresh checkpoint success", err) ++ } ++ if got := counterValue(t, stateDir, "checkpoint-count"); got != 2 { ++ t.Fatalf("checkpoint attempts = %d, want retry after discarding partial bytes", got) ++ } ++ if networkRestores != 1 { ++ t.Fatalf("network restores after retry = %d, want 1", networkRestores) ++ } ++ checkpoint, err := os.ReadFile(filepath.Join(checkpointPath, "checkpoint.img")) ++ if err != nil { ++ t.Fatalf("read recovered checkpoint: %v", err) ++ } ++ if got, want := string(checkpoint), "snapshot-2\n"; got != want { ++ t.Fatalf("recovered checkpoint = %q, want %q", got, want) ++ } ++ if valid, err := validateCheckpointRecovery(recoveryPath, expected); err != nil || !valid { ++ t.Fatalf("checkpoint recovery after interrupted write = valid:%t error:%v, want committed", valid, err) ++ } ++} ++ ++func TestCheckpointCleanupConvergesAfterDeleteRetryExhaustion(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "two-call-recovery", "pause", "app-a", "app-b") ++ rcmd := testRunsc(path) ++ checkpointPath := filepath.Join(t.TempDir(), "checkpoint") ++ recoveryPath := filepath.Join(t.TempDir(), checkpointRecoveryDirName) ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("create checkpoint directory: %v", err) ++ } ++ ++ podHasEth0 := false ++ interiorHasEth0 := true ++ networkMoves := 0 ++ restoreNetwork := func(ctx context.Context) error { ++ return ensurePodNetwork(ctx, podNetworkOps{ ++ podEth0Present: func(context.Context) (bool, error) { ++ return podHasEth0, nil ++ }, ++ interiorEth0Present: func(context.Context) (bool, error) { ++ return interiorHasEth0, nil ++ }, ++ moveInteriorEth0ToPod: func(context.Context) error { ++ networkMoves++ ++ podHasEth0 = true ++ interiorHasEth0 = false ++ return nil ++ }, ++ }) ++ } ++ ++ err := checkpointAndCleanup( ++ context.Background(), ++ rcmd, ++ []string{"app-a", "app-b"}, ++ checkpointPath, ++ recoveryPath, ++ restoreNetwork, ++ ) ++ if err == nil { ++ t.Fatal("first checkpointAndCleanup() error = nil, want exhausted delete retries") ++ } ++ if !strings.Contains(err.Error(), "container is still present") { ++ t.Fatalf("first checkpointAndCleanup() error = %q, want exhausted presence postcondition", err) ++ } ++ if got := counterValue(t, stateDir, "delete-count-app-b"); got != runscDeleteAttempts { ++ t.Fatalf("first-call app-b delete attempts = %d, want %d", got, runscDeleteAttempts) ++ } ++ if got, want := formatContainerSet(presentContainerSet(t, stateDir)), "app-b,pause"; got != want { ++ t.Fatalf("present containers after first call = %q, want %q", got, want) ++ } ++ if networkMoves != 1 || !podHasEth0 || interiorHasEth0 { ++ t.Fatalf("network after first call = moves:%d pod:%t interior:%t, want 1,true,false", networkMoves, podHasEth0, interiorHasEth0) ++ } ++ if got := counterValue(t, stateDir, "checkpoint-count"); got != 1 { ++ t.Fatalf("checkpoint attempts after first call = %d, want 1", got) ++ } ++ ++ // Atelet removes and recreates the normal checkpoint directory before every ++ // RPC retry. The second call must repopulate it from recovery rather than ++ // checkpointing a workload whose app-a container is already absent. ++ if err := os.RemoveAll(checkpointPath); err != nil { ++ t.Fatalf("clear checkpoint directory: %v", err) ++ } ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("recreate checkpoint directory: %v", err) ++ } ++ ++ if err := checkpointAndCleanup( ++ context.Background(), ++ rcmd, ++ []string{"app-a", "app-b"}, ++ checkpointPath, ++ recoveryPath, ++ restoreNetwork, ++ ); err != nil { ++ t.Fatalf("second checkpointAndCleanup() error = %v, want recovery success", err) ++ } ++ if got := counterValue(t, stateDir, "checkpoint-count"); got != 1 { ++ t.Fatalf("checkpoint attempts after second call = %d, want preserved first checkpoint", got) ++ } ++ if got := counterValue(t, stateDir, "delete-count-app-b"); got != runscDeleteAttempts+1 { ++ t.Fatalf("total app-b delete attempts = %d, want %d", got, runscDeleteAttempts+1) ++ } ++ if got := len(presentContainerSet(t, stateDir)); got != 0 { ++ t.Fatalf("present container count after recovery = %d, want 0", got) ++ } ++ if networkMoves != 1 { ++ t.Fatalf("network moves after second call = %d, want already-restored no-op", networkMoves) ++ } ++ checkpoint, err := os.ReadFile(filepath.Join(checkpointPath, "checkpoint.img")) ++ if err != nil { ++ t.Fatalf("read restored checkpoint: %v", err) ++ } ++ if got, want := string(checkpoint), "snapshot-1\n"; got != want { ++ t.Fatalf("restored checkpoint = %q, want %q", got, want) ++ } ++ if valid, err := validateCheckpointRecovery(recoveryPath, map[string]struct{}{ ++ "pause": {}, ++ "app-a": {}, ++ "app-b": {}, ++ }); err != nil || !valid { ++ t.Fatalf("checkpoint recovery after success = valid:%t error:%v, want retained valid copy", valid, err) ++ } ++} ++ ++func TestCheckpointCleanupFailsClosedOnPartialStateWithoutRecovery(t *testing.T) { ++ path, stateDir := writeFakeRunsc(t, "success", "pause", "app-b") ++ checkpointPath := filepath.Join(t.TempDir(), "checkpoint") ++ recoveryPath := filepath.Join(t.TempDir(), checkpointRecoveryDirName) ++ if err := os.MkdirAll(checkpointPath, 0o700); err != nil { ++ t.Fatalf("create checkpoint directory: %v", err) ++ } ++ networkCalled := false ++ err := checkpointAndCleanup( ++ context.Background(), ++ testRunsc(path), ++ []string{"app-a", "app-b"}, ++ checkpointPath, ++ recoveryPath, ++ func(context.Context) error { ++ networkCalled = true ++ return nil ++ }, ++ ) ++ if err == nil { ++ t.Fatal("checkpointAndCleanup() error = nil, want partial-state failure") ++ } ++ if !strings.Contains(err.Error(), "partial container state without complete checkpoint recovery") { ++ t.Fatalf("checkpointAndCleanup() error = %q, want fail-closed partial-state context", err) ++ } ++ if got := counterValue(t, stateDir, "checkpoint-count"); got != 0 { ++ t.Fatalf("checkpoint attempts = %d, want 0 before uncertain state is rejected", got) ++ } ++ if networkCalled { ++ t.Fatal("network restoration ran for uncertain partial state") ++ } ++} ++ ++func TestEnsurePodNetworkFailsClosedOnUncertainLocation(t *testing.T) { ++ for _, test := range []struct { ++ name string ++ pod bool ++ interior bool ++ }{ ++ {name: "missing from both namespaces", pod: false, interior: false}, ++ {name: "present in both namespaces", pod: true, interior: true}, ++ } { ++ t.Run(test.name, func(t *testing.T) { ++ moved := false ++ err := ensurePodNetwork(context.Background(), podNetworkOps{ ++ podEth0Present: func(context.Context) (bool, error) { ++ return test.pod, nil ++ }, ++ interiorEth0Present: func(context.Context) (bool, error) { ++ return test.interior, nil ++ }, ++ moveInteriorEth0ToPod: func(context.Context) error { ++ moved = true ++ return nil ++ }, ++ }) ++ if err == nil || !strings.Contains(err.Error(), "failed closed") { ++ t.Fatalf("ensurePodNetwork() error = %v, want fail-closed uncertainty", err) ++ } ++ if moved { ++ t.Fatal("ensurePodNetwork() moved eth0 from an uncertain state") ++ } ++ }) ++ } ++} ++ ++func TestPrepareCheckpointRestoreRemovesMarkedCompatibilityFiles(t *testing.T) { ++ dir := t.TempDir() ++ if err := os.WriteFile(filepath.Join(dir, "checkpoint.img"), []byte("checkpoint\n"), 0o600); err != nil { ++ t.Fatalf("write checkpoint: %v", err) ++ } ++ compatibility := map[string]string{ ++ "pages.img": checkpointPagesCompatibilityMarker, ++ "pages_meta.img": checkpointPagesMetadataCompatibilityMarker, ++ } ++ for name, marker := range compatibility { ++ if err := os.WriteFile(filepath.Join(dir, name), []byte(marker), 0o600); err != nil { ++ t.Fatalf("write %s: %v", name, err) ++ } ++ } ++ if err := prepareCheckpointRestore(dir); err != nil { ++ t.Fatalf("prepareCheckpointRestore() error = %v", err) ++ } ++ for name := range compatibility { ++ if _, err := os.Stat(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("compatibility file %s still exists: %v", name, err) ++ } ++ } ++} ++ ++func TestPrepareCheckpointRestoreRecoversPartialCompatibilityCleanup(t *testing.T) { ++ dir := t.TempDir() ++ if err := os.WriteFile( ++ filepath.Join(dir, "pages_meta.img"), ++ []byte(checkpointPagesMetadataCompatibilityMarker), ++ 0o600, ++ ); err != nil { ++ t.Fatalf("write surviving compatibility marker: %v", err) ++ } ++ if err := prepareCheckpointRestore(dir); err != nil { ++ t.Fatalf("prepareCheckpointRestore() partial cleanup error = %v", err) ++ } ++ if _, err := os.Stat(filepath.Join(dir, "pages_meta.img")); !errors.Is(err, os.ErrNotExist) { ++ t.Fatalf("surviving compatibility file still exists: %v", err) ++ } ++} ++ ++func TestPrepareCheckpointRestorePreservesNativeMultiFileSnapshot(t *testing.T) { ++ dir := t.TempDir() ++ for _, file := range []struct { ++ name string ++ data string ++ }{ ++ {name: "checkpoint.img", data: "checkpoint\n"}, ++ {name: "pages.img", data: ""}, ++ {name: "pages_meta.img", data: "metadata\n"}, ++ } { ++ if err := os.WriteFile(filepath.Join(dir, file.name), []byte(file.data), 0o600); err != nil { ++ t.Fatalf("write %s: %v", file.name, err) ++ } ++ } ++ if err := prepareCheckpointRestore(dir); err != nil { ++ t.Fatalf("prepareCheckpointRestore() error = %v", err) ++ } ++ if info, err := os.Stat(filepath.Join(dir, "pages.img")); err != nil || info.Size() != 0 { ++ t.Fatalf("native empty pages.img = info:%v error:%v, want preserved empty file", info, err) ++ } ++ if info, err := os.Stat(filepath.Join(dir, "pages_meta.img")); err != nil || info.Size() == 0 { ++ t.Fatalf("native pages_meta.img = info:%v error:%v, want preserved nonempty file", info, err) ++ } ++} ++ ++func TestPrepareCheckpointRestoreRejectsPartialNativeState(t *testing.T) { ++ dir := t.TempDir() ++ if err := os.WriteFile(filepath.Join(dir, "pages.img"), []byte("native-pages\n"), 0o600); err != nil { ++ t.Fatalf("write pages.img: %v", err) ++ } ++ if err := prepareCheckpointRestore(dir); err == nil { ++ t.Fatal("prepareCheckpointRestore() error = nil, want missing native metadata failure") ++ } ++} diff --git a/hack/demos/README.md b/hack/demos/README.md index dcc8047e6..c690b5831 100644 --- a/hack/demos/README.md +++ b/hack/demos/README.md @@ -6,8 +6,9 @@ This directory contains a small `demo-magic` kit for showing Orka in six ways: - `20-manual-workflow.sh`: explicit coordinator Task CR for a focused Vekil metrics first-PR workflow - `30-cron-workflow.sh`: scheduled runtime task with recurring child runs - `40-security-scanning.sh`: repository scan -> findings -> patch -> PR -- `60-agent-sandbox.sh`: three turns share a single SandboxClaim via `sessionRef` (scout -> builder -> CI fixup, same workspace) -- `70-agent-substrate.sh`: a real gpt-5.5 codex agent in a gVisor Actor (Agent Substrate) clones a repo, edits it, and opens a PR; a second task reuses the warm workspace with no cold start +- `50-kontxt.sh`: workload SA token -> in-cluster TTS -> request-scoped TxToken -> Orka API call (one identity, two outcomes) +- `60-agent-sandbox.sh`: archived execution-workspace prototype; not a current ACP v2 path +- `70-agent-substrate.sh`: archived Substrate prototype; requires an Actor-backed v2 supervisor before it is supported again There is also: @@ -21,7 +22,7 @@ There is also: - Optional: an upstream `demo-magic.sh` if you want to override the vendored fallback - A demo namespace that is not the controller namespace - A Provider CRD and runtime credential Secret in that demo namespace -- A git credential Secret in that demo namespace for clone, push, and PR creation +- Separate read/clone and publication/forge credential Secrets for any ACP workspace demo. The archived 60/70 scripts still assume one broad Git Secret and must be migrated before use. The demo scripts include a lightweight `demo-magic.sh` fallback at `hack/demos/lib/demo-magic.sh`, so no separate checkout is required. If you prefer the upstream `demo-magic` behavior, set `DEMO_MAGIC_PATH` to your local checkout. If `DEMO_MAGIC_PATH` points at a missing file, the scripts ignore it and use the vendored fallback. @@ -119,7 +120,7 @@ spec: YAML ``` -Create the runtime credential Secret for the Agent runtime. Codex accepts `OPENAI_API_KEY` or `CODEX_API_KEY`; Copilot accepts `GITHUB_TOKEN`; Claude accepts `ANTHROPIC_API_KEY`. +Create the provider/proxy credential Secret for the ACP runtime. Keep this role separate from both Git read and publication credentials. Codex accepts `OPENAI_API_KEY` or `CODEX_API_KEY`; Copilot accepts `GITHUB_TOKEN`; Claude accepts `ANTHROPIC_API_KEY`. ```bash # Codex example: @@ -133,12 +134,15 @@ kubectl -n "$DEMO_NAMESPACE" create secret generic "$DEMO_RUNTIME_SECRET_REF" \ # --dry-run=client -o yaml | kubectl apply -f - ``` -Create the git credential Secret used for clone, push, and PR creation: +For current ACP manifests, create separate repository credentials. The clean-room workspace boundary uses the read Secret; the Workspace/Publisher alone uses the publication Secret: ```bash -kubectl -n "$DEMO_NAMESPACE" create secret generic "$DEMO_GIT_SECRET_REF" \ - --from-literal=username='' \ - --from-literal=password='' \ +kubectl -n "$DEMO_NAMESPACE" create secret generic repository-read \ + --from-literal=token='' \ + --dry-run=client -o yaml | kubectl apply -f - + +kubectl -n "$DEMO_NAMESPACE" create secret generic repository-publish \ + --from-literal=token='' \ --dry-run=client -o yaml | kubectl apply -f - ``` @@ -283,11 +287,10 @@ source hack/demos/cluster/demo-env.sh make demo-cluster-up-all-down # tear it all down ``` -Notes: `install-agent-sandbox.sh` runs **last** in the bootstrap because it sets -the controller's default workspace provider to `agent-sandbox` (Demo 60 relies -on that default). Demo 70 sets `provider: substrate` explicitly, while the -model-backed demos continue to use their normal ServiceAccount authentication, -so the scenarios coexist safely. +Notes: the agent-sandbox/Substrate bootstrap and demos are retained only for +prototype archaeology. Current ACP v2 validation must not set a default execution +workspace provider or rely on a per-Task worker path. kontxt's `enforce` mode only +gates requests carrying a `Txn-Token`, so the other demos remain independent. Known flake (Demo 70): the warm-reuse Task occasionally fails during workspace release with a gVisor `RestoreWorkload: ... eth0: Link not found` daemon error diff --git a/hack/demos/RECORDING.md b/hack/demos/RECORDING.md index 7a39470a9..e8cadf679 100644 --- a/hack/demos/RECORDING.md +++ b/hack/demos/RECORDING.md @@ -1,5 +1,7 @@ # Demo recording design +> **ACP v2 cutover:** the Demo 60/70 storyboards below capture a retired execution-workspace prototype. Current built-in agent Tasks use RuntimePools, top-level `spec.workspace`, and the separate Workspace/Publisher. Keep these storyboards archived until the scripts and manifests are rebuilt around an ACP v2 supervisor; they are not release evidence. + This is the design doc for turning `hack/demos/` from a presenter rehearsal kit into a small, tasteful library of recorded terminal demos. The goal is to publish a handful of artifacts that hold up on the README, in the docs, and on @@ -69,8 +71,9 @@ Six demos total. Four exist; two are new. | 20 | YAML workflow | `20-manual-workflow.sh` | exists, needs polish | Same payload, declarative `Task` CR — GitOps-friendly | | 30 | Scheduled workflow | `30-cron-workflow.sh` | exists, needs polish | Cron-scheduled stale-PR triage report | | 40 | Security remediation | `40-security-scanning.sh` | exists, needs polish | Finding → patch proposal → reviewable PR | -| 60 | **Agent sandbox workspaces** | `60-agent-sandbox.sh` | **new** | One session, two agents, three turns — Scout, Builder, and a CI fixup share one warm sandbox | -| 70 | **Agent Substrate workspaces** | `70-agent-substrate.sh` | **new** | Real gpt-5.5 agent in a gVisor Actor clones + edits + opens a PR; warm reuse with no cold start | +| 50 | **Kontxt transaction tokens** | `50-kontxt.sh` | **new** | Caller Pod proves identity → kontxt mints TxToken → Orka stamps immutable provenance | +| 60 | **Agent sandbox workspaces** | `60-agent-sandbox.sh` | archived prototype | Requires a future sandbox-backed ACP v2 supervisor | +| 70 | **Agent Substrate workspaces** | `70-agent-substrate.sh` | archived prototype | Requires a future Actor-backed ACP v2 supervisor and clean-room publication | Demos 50 and 60 are designed in [§7](#7-new-scenario-storyboards). @@ -568,7 +571,7 @@ the successful Task and renders: ╰─────────────────────────────────────────────────────────────╯ ``` -### Demo 60 — Agent sandbox workspaces (`60-agent-sandbox.sh`) +### Demo 60 — Agent sandbox workspaces (`60-agent-sandbox.sh`, archived prototype) **Why this matters.** The other demos all show one-shot work. Demo 60 shows *continuity*: a coding session's repo, dependency cache, and built artifacts @@ -687,7 +690,7 @@ by an order of magnitude. --- -### Demo 70 — Agent Substrate workspaces (`70-agent-substrate.sh`) +### Demo 70 — Agent Substrate workspaces (`70-agent-substrate.sh`, archived prototype) **Why this matters.** Orka's execution workspace is provider-neutral. Demo 60 shows agent-sandbox; Demo 70 shows the *same Orka agent Task API* backed by a @@ -716,8 +719,7 @@ demo-magic cluster. Stand it up with `make demo-substrate-up` `scripts/agent-substrate-e2e.sh` standup (`KEEP_CLUSTER=1`) — Substrate control plane in `ate-system`, a `WorkerPool` + gVisor `ActorTemplate` (`orka-codex-ci` in `ate-demo`), Orka wired with `--substrate-*` flags; (2) builds a -**codex-capable Actor image** (the production `agent-harness-wrapper` — daemon + -codex CLI + git) and points the ActorTemplate at it; (3) deploys the **vekil** +**prototype codex-capable Actor image** (workspace daemon + Codex CLI + git; not a supported ACP v2 runtime image) and points the ActorTemplate at it; (3) deploys the **vekil** model proxy (one-time GitHub **device-code** login — the operator completes it from the pod logs, since a plain `gho_` gh token has no Copilot entitlement); (4) creates the model Secret (endpoint → vekil) and the git Secret. Requires @@ -729,15 +731,14 @@ comes from `GIT_TOKEN`/`GITHUB_TOKEN` or the local `gh` CLI. | # | Beat | What the audience sees | |---|------|------------------------| -| 1 | Cold | A Task with `provider: substrate` + `reusePolicy: session` + `sessionRef.create: true`. A fresh gVisor Actor; a real `gpt-5.5` agent clones the repo, edits a file, stops. Orka pushes the branch; the demo opens a real PR. `status.executionWorkspace.provider == substrate`. | -| 2 | PR | The demo opens the pull request via `gh` (the agent edited only — clean exit; Orka pushed). The real PR URL appears. | -| 3 | Warm | A second Task, same `sessionRef` (`create: false`). Reattaches the retained workspace: `status.executionWorkspace.reused == true` — repo already cloned, no cold start. A follow-up commit lands on the same PR. | +| 1 | Cold | Archived prototype: a fresh gVisor Actor hosted the agent. A revived ACP v2 version must start from a sanitized source artifact and leave publication to the Workspace/Publisher. | +| 2 | PR | Future v2 version requires an independently verified publisher receipt and Orka-owned PR reconciliation. | +| 3 | Warm | Future v2 version must resume one fenced RuntimeSession without replaying a prompt or bypassing validation/publication barriers. | -**Clean-exit contract (load-bearing).** The agent **edits files only** and -stops; Orka's `pushBranch` pushes the branch; the **demo script** opens/updates -the PR via `gh`. If the agent runs post-edit commands itself (git status, a PR -curl), a nonzero one makes the codex CLI exit 1 even though the work succeeded — -so the prompt forbids it. +**Clean-exit contract (load-bearing).** The ACP child edits files only. A +revived demo must freeze and validate the RuntimeSession, upload a durable delta, +and require the separate Workspace/Publisher to prepare, publish, verify, and +reconcile the PR. Runtime-local Git state is never publication authority. **gVisor contract (load-bearing).** The Task sets `ORKA_CODEX_DISABLE_SANDBOX=true`. Codex's inner bubblewrap sandbox cannot nest @@ -870,18 +871,18 @@ spec: - name: file_write - name: code_exec systemPrompt: | - You implement changes proposed by the scout. You read - /workspace/scout-report.md, apply the changes, run tests via - code_exec, and use the in-workspace git CLI (cloned and authenticated - by the agent runtime) to push branches and open pull requests - against sozercan/vekil. + You implement changes proposed by the scout. Read the supplied report, + edit the verified workspace, and run focused tests. Do not commit, alter + Git configuration/remotes, push, or create a pull request; Orka owns + clean-room publication. ``` **Tool-name caveat.** Built-in Orka tools verified against `internal/tools/common_constants.go` and `workers/ai/main_test.go`: `file_read`, `file_write`, `code_exec`, `web_search`, `web_fetch` are -real. There is *no* `open_pr` built-in tool — PR creation is done by the -agent runtime using `git` + `gh` (or the GitHub HTTP API) from inside the +real. There is *no* `open_pr` built-in tool. Under ACP v2, PR creation is an +Orka-owned Workspace/Publisher or governed GitHub-tool operation, never a +runtime-local `git`/forge action inside the sandbox workspace. The scout/builder split is enforced by `file_write` + `code_exec` *presence on builder, absence on scout*, not by an `open_pr` tool. @@ -924,15 +925,11 @@ Prompt files in `hack/demos/prompts/`: issue #77 (Prometheus metrics gap). Profile what's missing. Write your proposal to `/workspace/scout-report.md` with: counter names, where they go, test outline. Do not modify any vekil source."* -- `sandbox-turn-2-builder.txt` — *"Read `/workspace/scout-report.md`. - Implement the counters and tests in the vekil checkout under - `/workspace/vekil`. Push the branch with `git` and open a pull request - against sozercan/vekil (using `gh pr create`) with title 'Add Prometheus - /metrics endpoint (closes #77)'."* -- `sandbox-turn-3-fixup.txt` — *"CI on the open PR flagged that - `metrics_handler_test.go` is missing a test for the `error_total` - counter. Add it. Push as a fixup commit on the same branch. Do not - open a new PR."* +- `sandbox-turn-2-builder.txt` — must be rewritten to edit and test only. + A future ACP v2 demo supplies a verified workspace artifact and asks the + Workspace/Publisher to prepare, publish, verify, and reconcile the PR. +- `sandbox-turn-3-fixup.txt` — must be rewritten as another fenced write Task + against the claimed branch baseline; the ACP child must not commit or publish. ### Substrate manifests (Demo 70) @@ -946,12 +943,13 @@ demo only applies the Orka `Agent` + two `Task`s and opens the PR. All carry `codex`, model `gpt-5.5`. A real model run: `secretRef` (NOT `providerRef` — mutually exclusive with `runtime`) points at a Secret carrying `OPENAI_BASE_URL` (→ the in-cluster vekil proxy) + a placeholder - `OPENAI_API_KEY`. The system prompt tells the agent to edit files only and - stop (Orka pushes; the demo opens the PR). + `OPENAI_API_KEY`. The system prompt tells the agent to edit files only and stop. A revived v2 + demo requires the Workspace/Publisher to prepare, publish, verify, and reconcile the PR. - **Task** (`render_substrate_task `) — agent Task whose `execution.workspace` selects `provider: substrate` with - `templateRef` → `ate-demo/orka-codex-ci`, plus `agentRuntime.workspace` - (`gitRepo`, `branch`, `pushBranch`, `gitSecretRef`) and + `templateRef` → `ate-demo/orka-codex-ci`, plus top-level `spec.workspace` + (`intent`, `gitRepo`, `branch`, `readCredentialRef`, `publicationGitRepo`, + `publicationCredentialRef`, and `pushBranch`) and `env: ORKA_CODEX_DISABLE_SANDBOX=true` (gVisor is the sandbox). `reusePolicy` defaults to `session`; `cleanupPolicy` is `retain` for session tasks so the workspace stays warm. The 3rd arg sets `sessionRef.create`. @@ -1175,8 +1173,9 @@ and by the payoff cards): | 20 | "GitOps workflow" | "Same workflow from YAML. The agent isn't magic — it's a CR." | | 30 | "Scheduled work" | "Recurring AI triage queue — same auditable Task model, just add a `schedule:`." | | 40 | "Security remediation" | "Finding → patch proposal → reviewable PR. No human triage required." | -| 60 | "Warm agent sandboxes" | "One session, two agents, three turns. Scout, Builder, CI fixup — one warm workspace." | -| 70 | "Agent Substrate workspaces" | "A real agent clones, edits, and opens a PR from inside a gVisor sandbox — then reuses the warm workspace with no cold start." | +| 50 | "Kontxt transaction tokens" | "Zero-secret caller, one-shot transaction token, sealed Kubernetes provenance." | +| 60 | "Warm agent sandboxes" | "Archived until agent-sandbox hosts an ACP v2 RuntimeSession without weakening workspace governance." | +| 70 | "Agent Substrate workspaces" | "Archived until an Actor-backed ACP v2 supervisor and clean-room publication path are implemented." | --- @@ -1230,12 +1229,10 @@ writing the relevant render functions: as a placeholder. The implementer must inspect what `00-preflight.sh` applies (or what `cluster/cluster-up.sh` installs) and use that exact name in the scout/builder Agent specs. -- **`gh` is preinstalled in the sandbox image.** The builder prompt - assumes `gh pr create` works inside the sandbox. Phase 1's - `cluster/templates/orka-live-template.yaml` is the implementer's - source of truth — make sure the template's image bundles `git` + `gh` - + a writable workspace. If not, either add them to the template image - or swap the builder prompt to use the GitHub REST API (`curl + jq`). +- **SCM publication is outside the sandbox.** A future template must not + depend on Git publication tools or credentials inside the ACP process tree. + The Workspace/Publisher owns clone credentials, deterministic commit creation, + exact-ref publication, independent verification, and PR reconciliation. - **Sandbox claim name shape.** §7.5's `payoff_card_sandbox` extracts the claim name from `completed in sandbox workspace ` — that line is verified, but the literal claim *name* (`orka-vekil-metrics-77` in the @@ -1265,8 +1262,8 @@ writing the relevant render functions: are needed. - `install-agent-sandbox.sh` — installs the upstream `agent-sandbox` operator via its published manifests, then applies - `cluster/templates/orka-live-template.yaml` (a `SandboxTemplate` - containing the agent CLI runtime image + git/gh + workspace dirs). + `cluster/templates/orka-live-template.yaml` (an archived `SandboxTemplate`; + replace it with an ACP v2 supervisor image before reviving the demo). - `cluster-down.sh` — `kind delete cluster --name orka-demo`. - [ ] Makefile — add `demo-record-%`, `demo-record-hero`, `demo-record-all`, `demo-diff`, `demo-images`, **`demo-cluster-up`**, **`demo-cluster-down`** diff --git a/hack/demos/cluster/cluster-down.sh b/hack/demos/cluster/cluster-down.sh index 46918985c..7e133c4a5 100755 --- a/hack/demos/cluster/cluster-down.sh +++ b/hack/demos/cluster/cluster-down.sh @@ -4,6 +4,10 @@ set -Eeuo pipefail cluster_name="${ORKA_DEMO_CLUSTER:-orka-demo}" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +# shellcheck source=scripts/lib/kind-local-registry.sh +. "${repo_root}/scripts/lib/kind-local-registry.sh" if kind get clusters | grep -qx "${cluster_name}"; then printf '==> Deleting kind cluster %s\n' "${cluster_name}" >&2 @@ -11,3 +15,5 @@ if kind get clusters | grep -qx "${cluster_name}"; then else printf '==> kind cluster %s not found; nothing to do\n' "${cluster_name}" >&2 fi + +orka_kind_registry_stop "${cluster_name}" diff --git a/hack/demos/cluster/cluster-up.sh b/hack/demos/cluster/cluster-up.sh index 826727373..e93b28f9c 100755 --- a/hack/demos/cluster/cluster-up.sh +++ b/hack/demos/cluster/cluster-up.sh @@ -9,11 +9,14 @@ set -Eeuo pipefail cluster_name="${ORKA_DEMO_CLUSTER:-orka-demo}" img="${ORKA_DEMO_IMAGE:-orka-demo:dev}" +publisher_img="${ORKA_DEMO_PUBLISHER_IMAGE:-orka-workspace-publisher:demo}" namespace="${ORKA_NAMESPACE:-orka-system}" demo_namespace="${DEMO_NAMESPACE:-demo-magic}" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/../../.." && pwd)" +# shellcheck source=scripts/lib/kind-local-registry.sh +. "${repo_root}/scripts/lib/kind-local-registry.sh" log() { printf '==> %s\n' "$*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } @@ -21,6 +24,9 @@ die() { printf 'error: %s\n' "$*" >&2; exit 1; } command -v kind >/dev/null 2>&1 || die "missing required command: kind" command -v docker >/dev/null 2>&1 || die "missing required command: docker" command -v kubectl >/dev/null 2>&1 || die "missing required command: kubectl" +command -v curl >/dev/null 2>&1 || die "missing required command: curl" +command -v jq >/dev/null 2>&1 || die "missing required command: jq" +[[ "${namespace}" == "orka-system" ]] || die "demo cluster currently requires ORKA_NAMESPACE=orka-system" if kind get clusters | grep -qx "${cluster_name}"; then log "kind cluster ${cluster_name} already exists; reusing" @@ -32,30 +38,34 @@ fi log "Selecting kubectl context kind-${cluster_name}" kubectl config use-context "kind-${cluster_name}" >/dev/null +orka_kind_registry_start "${cluster_name}" + log "Building controller image ${img}" (cd "${repo_root}" && make docker-build IMG="${img}") +log "Building workspace publisher image ${publisher_img}" +(cd "${repo_root}" && make docker-build-workspace-publisher WORKSPACE_PUBLISHER_IMG="${publisher_img}") log "Loading ${img} into kind/${cluster_name}" kind load docker-image "${img}" --name "${cluster_name}" +manager_ref="$(orka_kind_registry_push "${img}" "orka/controller")" +publisher_ref="$(orka_kind_registry_push "${publisher_img}" "orka/workspace-publisher")" -log "Ensuring namespace ${namespace} and ${demo_namespace}" +log "Ensuring namespaces ${namespace}, ${demo_namespace}, and vekil-system" kubectl create namespace "${namespace}" --dry-run=client -o yaml | kubectl apply -f - kubectl create namespace "${demo_namespace}" --dry-run=client -o yaml | kubectl apply -f - +kubectl create namespace vekil-system --dry-run=client -o yaml | kubectl apply -f - -log "Deploying Orka (namespace ${namespace}, image ${img})" -if [[ "${namespace}" == "orka-system" ]]; then - (cd "${repo_root}" && make deploy IMG="${img}") -else - (cd "${repo_root}" && make manifests kustomize) - tmp_config="$(mktemp -d)" - cp -R "${repo_root}/config" "${tmp_config}/config" - (cd "${tmp_config}/config/manager" && "${repo_root}/bin/kustomize" edit set image controller="${img}") - perl -0pi -e "s#--controller-url=http://orka-api\.orka-system\.svc:8080#--controller-url=http://orka-api.${namespace}.svc:8080#g" \ - "${tmp_config}/config/manager/manager.yaml" - (cd "${tmp_config}/config/default" && "${repo_root}/bin/kustomize" edit set namespace "${namespace}") - "${repo_root}/bin/kustomize" build "${tmp_config}/config/default" | kubectl apply -f - - rm -rf "${tmp_config}" -fi +log "Installing ACP v2 CRDs" +(cd "${repo_root}" && make install) + +log "Deploying Orka (namespace ${namespace}, image ${manager_ref})" +placeholder_digest="sha256:$(printf '0%.0s' {1..64})" +(cd "${repo_root}" && make deploy \ + IMG="${manager_ref}" \ + WORKSPACE_PUBLISHER_IMG="${publisher_ref}" \ + ACP_CODEX_RUNTIME_IMG="example.invalid/orka/acp-codex@${placeholder_digest}" \ + ACP_CLAUDE_RUNTIME_IMG="example.invalid/orka/acp-claude@${placeholder_digest}" \ + ACP_COPILOT_RUNTIME_IMG="example.invalid/orka/acp-copilot@${placeholder_digest}") log "Waiting for orka-controller-manager rollout" kubectl -n "${namespace}" rollout status deployment/orka-controller-manager --timeout=300s diff --git a/hack/demos/cluster/install-demo-model.sh b/hack/demos/cluster/install-demo-model.sh index 9464f346a..b8f294af2 100755 --- a/hack/demos/cluster/install-demo-model.sh +++ b/hack/demos/cluster/install-demo-model.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Provision the model Provider + secrets the model-backed demos (10/20/30/40) -# need, pointing them at the in-cluster vekil proxy. The workspace demos -# (50/60/70) bring their own model wiring; this script covers the SDLC demos. +# need, pointing them at the in-cluster vekil proxy. This script covers the +# active SDLC demos only; archived execution-workspace demos 60/70 are not +# configured here. # # What it creates in the demo namespace (DEMO_NAMESPACE, default demo-magic): # - a Provider CR (DEMO_PROVIDER_REF) used by the type: ai coordinator in @@ -9,8 +10,8 @@ # (demo 10 requires Opus). The provider api-key is a placeholder; vekil # holds the real Copilot session. # - the provider api-key Secret (DEMO_PROVIDER_SECRET_REF). -# - the runtime Secret (DEMO_RUNTIME_SECRET_REF) for the CLI agents: -# OPENAI_BASE_URL -> vekil /v1 + placeholder OPENAI_API_KEY (=> codex). +# - the ACP provider credential Secret (DEMO_RUNTIME_SECRET_REF): +# OPENAI_BASE_URL -> vekil /v1 + placeholder OPENAI_API_KEY (=> Codex). # - a git Secret (DEMO_GIT_SECRET_REF) with username/password (PR demos) AND # a token key (demo 30 reads GH_TOKEN from the 'token' key). Token from # GIT_TOKEN/GITHUB_TOKEN or the local gh CLI; never printed. @@ -39,8 +40,6 @@ command -v jq >/dev/null 2>&1 || die "missing required command: jq" orka_namespace="${ORKA_NAMESPACE:-orka-system}" controller_deployment="${ORKA_CONTROLLER_DEPLOYMENT:-orka-controller-manager}" -harness_wrapper_deployment="${ORKA_HARNESS_WRAPPER_DEPLOYMENT:-orka-agent-harness-wrapper}" -codex_image="${DEMO_HARNESS_WRAPPER_IMAGE:-localhost:${KIND_REGISTRY_PORT:-5001}/orka/agent-harness-wrapper:demo}" ai_image="${DEMO_AI_WORKER_IMAGE:-localhost:${KIND_REGISTRY_PORT:-5001}/orka/ai-worker:demo}" general_image="${DEMO_GENERAL_WORKER_IMAGE:-localhost:${KIND_REGISTRY_PORT:-5001}/orka/general-worker:demo}" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" @@ -56,7 +55,6 @@ require_image_source() { die "${label} build is disabled but ${image_var} is not set to an existing image" fi } -require_image_source "harness wrapper" "${DEMO_BUILD_CODEX_IMAGE:-1}" DEMO_HARNESS_WRAPPER_IMAGE require_image_source "AI worker" "${DEMO_BUILD_AI_IMAGE:-1}" DEMO_AI_WORKER_IMAGE require_image_source "general worker" "${DEMO_BUILD_GENERAL_IMAGE:-1}" DEMO_GENERAL_WORKER_IMAGE if [[ "${needs_docker}" == "1" ]]; then @@ -110,8 +108,8 @@ spec: defaultModel: ${provider_model} YAML -# --- Runtime Secret (CLI agents: codex via vekil /v1) ----------------------- -log "Creating runtime Secret ${demo_namespace}/${runtime_secret} (endpoint -> vekil)" +# --- ACP provider credential Secret (Codex via vekil /v1) ------------------ +log "Creating ACP provider credential Secret ${demo_namespace}/${runtime_secret} (endpoint -> vekil)" kubectl -n "${demo_namespace}" create secret generic "${runtime_secret}" \ --from-literal=OPENAI_BASE_URL="${vekil_url}" \ --from-literal=OPENAI_API_KEY=proxy-placeholder \ @@ -135,20 +133,14 @@ else log " kubectl -n ${demo_namespace} create secret generic ${git_secret} --from-literal=username=oauth2 --from-literal=password= --from-literal=token=" fi -# --- Git-capable codex worker image ----------------------------------------- -# The model-backed demos (10/20/30/40) run the agent directly in the worker pod -# (no sandbox/substrate workspace), so the worker image itself must contain git -# to clone the repo. The Substrate e2e deploys Orka with the STRIPPED codex -# image (workers/harness/Dockerfile = distroless, NO git), -# which fails these demos with "git: executable not found". Build the PRODUCTION -# codex image (workers/harness/Dockerfile has git + codex) and repoint the -# harness wrapper deployment at it. +# Built-in coding-agent runtimes are controller-owned, digest-pinned ACP +# RuntimePools. This demo helper intentionally does not build, repoint, or set +# process-wide sandbox environment on those runtimes. Install Orka with the +# desired immutable Codex/Claude/Copilot runtime images before running agent demos. + # build_and_repoint_worker