diff --git a/.github/workflows/live-copilot-proxy-e2e.yml b/.github/workflows/live-copilot-proxy-e2e.yml index 3c2f8ddfd..eca737d2f 100644 --- a/.github/workflows/live-copilot-proxy-e2e.yml +++ b/.github/workflows/live-copilot-proxy-e2e.yml @@ -26,7 +26,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} KIND_CLUSTER: orka-live-copilot-proxy-e2e - COPILOT_PROXY_IMAGE: ghcr.io/sozercan/vekil:v0.14.0@sha256:9e6ab58b9c27888db34d76422c3520b3bf103742a058572439a1fe0aa35a2ade + COPILOT_PROXY_IMAGE: ghcr.io/sozercan/vekil:v0.14.1@sha256:2fa0558f6304cc6ed1fb5b0135f62f12f28f1cdd0a8c057c4283414bceac1362 steps: - name: Decide whether live e2e can run id: gate diff --git a/charts/orka/README.md b/charts/orka/README.md index 270dc54fd..c6556204d 100644 --- a/charts/orka/README.md +++ b/charts/orka/README.md @@ -2,171 +2,33 @@ 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 26 canonical Orka CRDs under `crds/`. +packages all twelve canonical Orka CRDs under `crds/`. ## Fresh install -A normal `harness-v2` install requires Vekil to be running in `vekil-system`, -immutable controller and Publisher image digests, and two operator-managed -Secrets. Prepare: - -- a snapshot key file containing exactly 32 random bytes; -- a webhook serving certificate and private key whose certificate is valid for - `orka-webhook.orka-system.svc`, plus its PEM CA certificate; and -- `CONTROLLER_DIGEST` and `PUBLISHER_DIGEST` values in - `sha256:<64 lowercase hexadecimal characters>` form. - -The following creates the namespace and required Secrets without putting key -material in Helm values or command-line arguments, then installs the CRDs and -release resources. Replace the file paths and digest placeholders first: +A normal install creates the CRDs before the templated release resources: ```bash -set -euo pipefail - -: "${SNAPSHOT_KEY_FILE:?set SNAPSHOT_KEY_FILE to the 32-byte key file}" -: "${WEBHOOK_CERT_FILE:?set WEBHOOK_CERT_FILE to the serving certificate}" -: "${WEBHOOK_PRIVATE_KEY_FILE:?set WEBHOOK_PRIVATE_KEY_FILE to the private key}" -: "${WEBHOOK_CA_FILE:?set WEBHOOK_CA_FILE to the CA certificate}" -: "${CONTROLLER_DIGEST:?set CONTROLLER_DIGEST to sha256:<64 lowercase hex>}" -: "${PUBLISHER_DIGEST:?set PUBLISHER_DIGEST to sha256:<64 lowercase hex>}" - -kubectl create -f - <<'EOF' -apiVersion: v1 -kind: Namespace -metadata: - name: orka-system - labels: - orka.ai/controller-mode: harness-v2 -EOF -kubectl -n orka-system create secret generic agent-execution-snapshot-key \ - --from-file=snapshot-key="${SNAPSHOT_KEY_FILE}" -kubectl -n orka-system create secret generic orka-webhook-tls \ - --type=kubernetes.io/tls \ - --from-file=tls.crt="${WEBHOOK_CERT_FILE}" \ - --from-file=tls.key="${WEBHOOK_PRIVATE_KEY_FILE}" \ - --from-file=ca.crt="${WEBHOOK_CA_FILE}" - -WEBHOOK_CA_BUNDLE="$(kubectl -n orka-system get secret orka-webhook-tls \ - -o jsonpath='{.data.ca\.crt}')" - helm install orka charts/orka \ --namespace orka-system \ - --set controller.mode=harness-v2 \ - --set controller.watchNamespace=orka-system \ - --set-string controller.image.digest="${CONTROLLER_DIGEST}" \ - --set-string publisher.image.digest="${PUBLISHER_DIGEST}" \ - --set-string controller.agentExecutionSnapshot.existingSecret=agent-execution-snapshot-key \ - --set-string controller.agentExecutionSnapshot.key=snapshot-key \ - --set-string webhooks.tls.existingSecret=orka-webhook-tls \ - --set-string webhooks.caBundle="${WEBHOOK_CA_BUNDLE}" \ - --set providerProxy.enabled=true \ + --create-namespace \ --wait ``` -The chart installs the exact cross-namespace ingress policy for Vekil. 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. - -### SCM proxy NetworkPolicy portability boundary - -The SCM proxy NetworkPolicy excludes RFC 1918 and reserved address ranges, but -Kubernetes does not define whether Service destination NAT runs before or -after `ipBlock` evaluation. Some CNI and cloud combinations can therefore -reach the `kubernetes.default` transport through its ClusterIP despite those -exclusions. This does not grant API authorization: the SCM proxy Pod and -ServiceAccount do not mount a service-account token, service links are disabled, -Orka grants that identity no API RBAC, and the proxy refuses to start if the -conventional Kubernetes service-account token path exists. The proxy itself -accepts only exact configured SCM hostnames and rejects non-public DNS answers -and connected peers. Clusters requiring TCP-level API denial must add and -validate a CNI- or cloud-native pre-DNAT or Service-aware egress control; -standard NetworkPolicy cannot guarantee this portably. - -### 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. - -The harness-v1 wrapper likewise keeps execution authority and transport -material separate. `harnessV1.auth.existingSecret` contains only the bearer -token and is immutable while v1 work exists. `harnessV1.tls.existingSecret` -contains `tls.crt`, `tls.key`, and `ca.crt`. A TLS Secret name change is a -wrapper Pod-template change and automatically uses the existing drained -rollover. For same-name certificate renewal, update the TLS Secret and bump -`harnessV1.tls.rolloutNonce` in the Helm upgrade; the hook drains the live -wrapper before both wrapper and controller restart. Keep the updated `ca.crt` -able to verify the certificate currently being served during that drain, or -rotate to a versioned TLS Secret so the hook can mount the prior CA. - 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. -## Static harness mode - -Every release selects exactly one controller mode: `harness-v1` or -`harness-v2`. `dual`, `auto`, and `harness-v1-drain` are rejected. Each release -also requires a distinct, non-empty `controller.watchNamespace` labeled with -the matching mode: - -```bash -kubectl create -f - <<'EOF' -apiVersion: v1 -kind: Namespace -metadata: - name: orka-v2-system - labels: - orka.ai/controller-mode: harness-v2 -EOF - -helm install orka-v2 charts/orka \ - --namespace orka-v2-system \ - --set controller.mode=harness-v2 \ - --set controller.watchNamespace=orka-v2-system -``` - -The mode is an installation identity, not an upgrade toggle. Never change a -release from v1 to v2 in place or reuse its PVC, SQLite store, ledger, Session, -or Task identities under the other mode. - -A v1 and v2 release may share a cluster only when their release/watch -namespaces, Services, ServiceAccounts/RBAC, Leases, stores, Secrets, and -data-plane resources are disjoint. The chart intentionally requires -`controller.watchNamespace` to equal the Helm release namespace. The v2 -release must also have its own runtime namespace. Install the shared compatible -CRDs and common admission resources through one designated owner; install the -second release with `--skip-crds`. - Controller Services, worker ServiceAccounts, and worker RBAC are scoped to the Helm release name. Run only one Orka controller release per namespace. If a cluster has multiple releases, every release (including the first) must use a cluster-unique release name or `fullnameOverride`, a separate controller -namespace, and a distinct, non-empty `controller.watchNamespace`. Cluster-wide -watchers are rejected. All releases share the same cluster-scoped CRDs, and -cluster-scoped gateway/workspace ownership belongs only to the v2 release. +namespace, and a distinct, non-empty `controller.watchNamespace`. Do not mix a +cluster-wide watcher with namespace-scoped releases: gateway admission policies +would overlap. All releases share the same cluster-scoped CRDs. ## Upgrade -An in-place controller upgrade is supported only when the release namespace -already carries the same static mode claim and any live controller declares -that mode and watch namespace. A deleted controller can be recreated only -under that retained same-mode namespace claim. A pre-static controller that -implicitly enabled ACP is not a supported `harness-v2` upgrade source because -its accepted attempts may lack the immutable execution authority required for -recovery. Settle or retire that installation and install static `harness-v2` -as a new release and namespace. The chart rejects missing, opposite-mode, and -legacy identity before rendering upgrade resources. - Helm installs files from `crds/` only during installation. It does not create or update them during `helm upgrade`, including when upgrading from an older Orka chart that installed no CRDs. @@ -214,7 +76,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 26 CRDs to become `Established`, and then upgrade Orka. +wait for all twelve 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 76f7d04c9..cfa5071b5 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 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. + 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: |- @@ -1105,18 +1105,6 @@ 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. @@ -1150,13 +1138,6 @@ 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, @@ -1225,35 +1206,13 @@ 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: |- Model defines the LLM model configuration Provider field is optional if providerRef is set properties: - contextWindow: - description: |- - ContextWindow is the reviewed model context capacity in tokens. Built-in - runtimes that manage their own compaction require this value explicitly. - format: int32 - minimum: 1 - type: integer fallbacks: description: |- Fallbacks defines alternative providers to try when the primary fails. @@ -1274,10 +1233,7 @@ spec: type: object type: array maxTokens: - description: |- - MaxTokens limits the response length. OpenCode validates positive reviewed - limits at its runtime-specific admission boundary; existing Agent objects - may retain the legacy zero value. + description: MaxTokens limits the response length format: int32 type: integer name: @@ -1294,6 +1250,7 @@ spec: - openai type: string temperature: + default: 0.7 description: Temperature controls randomness in generation maximum: 2 minimum: 0 @@ -1391,17 +1348,6 @@ spec: 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: - contractVersion: - description: |- - ContractVersion is the immutable harness protocol selector for built-in - runtime types. There is no default: a missing selector is never - interpreted as either protocol, and fail-closed admission requires an - explicit value on new built-in Agents. runtime.type alone (including - opencode, which exists in both protocols) is never protocol evidence. - enum: - - orka.harness.v1 - - orka.harness.v2 - type: string defaultAllowBash: description: |- DefaultAllowBash controls whether bash is allowed by default for tasks using this Agent. @@ -1421,17 +1367,6 @@ 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. @@ -1448,21 +1383,15 @@ spec: description: Type specifies which built-in CLI runtime to use. Use runtimeRef for admin-registered custom runtimes. enum: + - copilot - claude - codex - - copilot - opencode type: string type: object x-kubernetes-validations: - message: exactly one of type or runtimeRef is required rule: has(self.type) != has(self.runtimeRef) - - message: runtime.contractVersion is immutable once set - rule: '!has(oldSelf.contractVersion) || (has(self.contractVersion) - && self.contractVersion == oldSelf.contractVersion)' - - message: runtime.contractVersion applies only to built-in runtime - types; runtimeRef derives the protocol from the referenced AgentRuntime - rule: '!has(self.contractVersion) || has(self.type)' secretRef: description: SecretRef references a Secret containing LLM API keys properties: @@ -1546,9 +1475,6 @@ 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: @@ -1573,14 +1499,6 @@ 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)' - - message: opencode orka.harness.v2 runtime does not support spec.systemPrompt - rule: '!(has(self.runtime) && has(self.runtime.type) && self.runtime.type - == ''opencode'' && has(self.runtime.contractVersion) && self.runtime.contractVersion - == ''orka.harness.v2'' && has(self.systemPrompt) && ((has(self.systemPrompt.inline) - && self.systemPrompt.inline.size() > 0) || has(self.systemPrompt.configMapRef)))' 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 707f8640a..e9623b7ae 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.runtimeInstanceID - name: Instance + - jsonPath: .status.observedCapabilities.runtimeName + name: Runtime type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -33,8 +33,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: AgentRuntime is the Schema for registered external Orka harness - runtimes. + description: AgentRuntime is the Schema for registered Orka harness runtimes. properties: apiVersion: description: |- @@ -54,27 +53,19 @@ spec: metadata: type: object spec: - description: |- - AgentRuntimeRegistrySpec defines the desired state of a registered Orka harness runtime. - The dual schema has no contractVersion default: omission is tolerated only for - stored objects awaiting the one-time bridge classification and is never - interpreted as either protocol. Fail-closed admission requires an explicit - value for new registrations. + description: AgentRuntimeRegistrySpec defines the desired state of a registered + Orka harness runtime. properties: capabilities: - description: |- - Capabilities pins the runtime capability claims. Required with the exact - instance/profile/limits/governance shape for orka.harness.v2; historically - optional for orka.harness.v1. + description: Capabilities declares readiness requirements Orka checks + against the runtime. properties: brokeredToolClasses: - description: |- - BrokeredToolClasses lists the harness v1 brokered tool classes supported - by the runtime. v1 only; historically optional. + description: BrokeredToolClasses lists brokered tool classes the + runtime must advertise when brokered mode is required. items: - description: |- - AgentRuntimeBrokeredToolClass classifies Tool CRDs. It is shared by the Tool - API and by harness v1 AgentRuntime capability declarations. + description: AgentRuntimeBrokeredToolClass declares which classes + of Orka-brokered tools a runtime can request. enum: - read - write @@ -82,293 +73,44 @@ spec: type: string type: array x-kubernetes-list-type: set - limits: - description: Limits must exactly match /v2/capabilities. v2 only. - 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 - v2 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 - modelLimits: - description: ModelLimits pins optional reviewed model token - capacities. - properties: - context: - description: Context is the maximum model context capacity - in tokens. - format: int64 - minimum: 1 - type: integer - output: - description: Output is the maximum generated output in - tokens. - format: int64 - minimum: 1 - type: integer - required: - - context - - output - type: object - x-kubernetes-validations: - - message: model context limit must exceed output limit - rule: self.context > self.output - 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. v2 only. - maxLength: 253 - minLength: 1 - type: string supportsArtifacts: - description: SupportsArtifacts declares harness v1 artifact support. - v1 only. + description: SupportsArtifacts requires the runtime to advertise + artifact/result reference support when true. type: boolean supportsCancel: - description: SupportsCancel declares harness v1 turn cancellation - support. v1 only. + description: SupportsCancel requires the runtime to advertise + cancellation support when true. type: boolean supportsContinuation: - description: SupportsContinuation declares harness v1 brokered - continuation support. v1 only. - type: boolean - supportsDrain: - description: SupportsDrain must exactly match the static v2 capability - claim. - type: boolean - supportsPublicationFinalization: - description: SupportsPublicationFinalization must exactly match - the static v2 capability claim. + description: SupportsContinuation requires the runtime to advertise + continuation after Orka-brokered tool results when true. type: boolean supportsRuntimeSessions: - description: SupportsRuntimeSessions declares harness v1 runtime - session support. v1 only. + description: SupportsRuntimeSessions requires the runtime to advertise + stable runtime sessions when true. type: boolean toolExecutionModes: - description: |- - ToolExecutionModes lists the harness v1 tool execution modes supported by - the runtime. v1 only; historically optional. + description: ToolExecutionModes lists tool execution modes the + runtime must advertise. items: - description: AgentRuntimeToolExecutionMode describes how a harness - v1 runtime executes tools. + 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 - v2 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) type: object clientAuth: - description: ClientAuth configures controller authentication and mutation - authorization. + description: ClientAuth configures controller-to-runtime authentication. properties: bearerTokenSecretRef: description: |- - BearerAuthRef points to the harness v1 bearer token Secret used for - mutating v1 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. + 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 @@ -383,80 +125,28 @@ spec: - key - name type: object - 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: |- - OperationCapabilitySecretRef supplies the HMAC secret used to bind every - v2 mutation to its exact fence, operation identity, request digest, and expiry. - 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 + required: + - bearerTokenSecretRef type: object - x-kubernetes-validations: - - message: legacy v1 and v2 client auth shapes are mutually exclusive - rule: '!(has(self.bearerTokenSecretRef) && (has(self.controllerBearerTokenSecretRef) - || has(self.operationCapabilitySecretRef)))' - - message: v2 client auth requires both controllerBearerTokenSecretRef - and operationCapabilitySecretRef - rule: has(self.controllerBearerTokenSecretRef) == has(self.operationCapabilitySecretRef) - - message: client auth requires either the v1 or the v2 credential - shape - rule: has(self.bearerTokenSecretRef) || has(self.controllerBearerTokenSecretRef) contractVersion: - description: |- - ContractVersion is the Orka harness contract this runtime must implement. - It is immutable once set. Required for new registrations through - fail-closed admission; the bridge schema tolerates omission only on - unchanged stored objects while execution admission is closed. + default: orka.harness.v1 + 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 an external harness service. It must not - contain credentials, query parameters, or fragments. + 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. External AgentRuntime registrations are not - scaled or recycled by Orka. + description: Mode is the deployment mode. The first milestone + supports external endpoints only. enum: - external-endpoint type: string @@ -466,40 +156,9 @@ spec: type: object required: - clientAuth + - contractVersion - deployment type: object - x-kubernetes-validations: - - message: contractVersion is immutable once set - rule: '!has(oldSelf.contractVersion) || (has(self.contractVersion) && - self.contractVersion == oldSelf.contractVersion)' - - message: orka.harness.v1 requires the legacy bearerTokenSecretRef client - auth shape - rule: '!has(self.contractVersion) || self.contractVersion != ''orka.harness.v1'' - || (has(self.clientAuth.bearerTokenSecretRef) && !has(self.clientAuth.controllerBearerTokenSecretRef) - && !has(self.clientAuth.operationCapabilitySecretRef))' - - message: orka.harness.v1 capabilities must not carry v2 capability fields - rule: '!has(self.contractVersion) || self.contractVersion != ''orka.harness.v1'' - || !has(self.capabilities) || (!has(self.capabilities.runtimeInstanceID) - && !has(self.capabilities.profile) && !has(self.capabilities.limits) - && !has(self.capabilities.workspaceGovernance) && !has(self.capabilities.supportsDrain) - && !has(self.capabilities.supportsPublicationFinalization))' - - message: orka.harness.v2 requires the v2 controller bearer and operation - capability client auth shape - rule: '!has(self.contractVersion) || self.contractVersion != ''orka.harness.v2'' - || (has(self.clientAuth.controllerBearerTokenSecretRef) && has(self.clientAuth.operationCapabilitySecretRef) - && !has(self.clientAuth.bearerTokenSecretRef))' - - message: orka.harness.v2 requires pinned instance, profile, limits, - and workspace governance capabilities - rule: '!has(self.contractVersion) || self.contractVersion != ''orka.harness.v2'' - || (has(self.capabilities) && has(self.capabilities.runtimeInstanceID) - && has(self.capabilities.profile) && has(self.capabilities.limits) - && has(self.capabilities.workspaceGovernance))' - - message: orka.harness.v2 capabilities must not carry v1 capability fields - rule: '!has(self.contractVersion) || self.contractVersion != ''orka.harness.v2'' - || !has(self.capabilities) || (!has(self.capabilities.toolExecutionModes) - && !has(self.capabilities.brokeredToolClasses) && !has(self.capabilities.supportsCancel) - && !has(self.capabilities.supportsRuntimeSessions) && !has(self.capabilities.supportsContinuation) - && !has(self.capabilities.supportsArtifacts))' status: description: AgentRuntimeStatus defines the observed state of an AgentRuntime. properties: @@ -573,26 +232,20 @@ spec: type: string observedAuthRefResourceVersion: description: |- - ObservedAuthRefResourceVersion is the resourceVersion of the harness v1 - bearer auth Secret used for the last v1 readiness probe. It is non-secret - metadata used to decide when token rotation requires a fresh authenticated - conformance turn. v2 probes use the two v2 auth resource-version fields. + 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: - acpVersion: - type: string - adapterDigest: - type: string - adapterName: - type: string brokeredToolClasses: + description: BrokeredToolClasses are the brokered tool classes + advertised by /v1/capabilities. items: - description: |- - AgentRuntimeBrokeredToolClass classifies Tool CRDs. It is shared by the Tool - API and by harness v1 AgentRuntime capability declarations. + description: AgentRuntimeBrokeredToolClass declares which classes + of Orka-brokered tools a runtime can request. enum: - read - write @@ -600,123 +253,63 @@ spec: type: string type: array x-kubernetes-list-type: set - controllerEpoch: - format: int64 - type: integer - lifecycle: - type: string - limits: - description: Limits records the v2 protocol bounds. It is absent - for harness v1. - 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 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: - type: integer - model: - type: string - profileDigestSchemaVersion: - format: int32 + 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: - type: string - runtimeInstanceID: + description: ProviderKind is the provider kind advertised by /v1/capabilities. type: string runtimeName: - type: string - runtimePoolGeneration: - format: int64 - type: integer - runtimePoolUID: - type: string - runtimeProfileDigest: + description: RuntimeName is the runtime name advertised by /v1/capabilities. type: string runtimeVersion: - type: string - supervisorBootID: + 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: - type: boolean - supportsDrain: - type: boolean - supportsPublicationFinalization: + 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 describes how a harness - v1 runtime executes tools. + description: AgentRuntimeToolExecutionMode declares how custom + runtimes interact with tools. enum: - observed - brokered @@ -724,83 +317,14 @@ spec: type: array x-kubernetes-list-type: set transport: + description: Transport is the runtime transport, normally http+sse. type: string - workspaceGovernance: - description: WorkspaceGovernance records the v2 workspace guarantees. - It is absent for harness v1. - 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 deleted file mode 100644 index 1328a8fe0..000000000 --- a/charts/orka/crds/branchclaim-customresourcedefinition.yaml +++ /dev/null @@ -1,197 +0,0 @@ ---- -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 deleted file mode 100644 index 9d1c486f2..000000000 --- a/charts/orka/crds/controllerepoch-customresourcedefinition.yaml +++ /dev/null @@ -1,109 +0,0 @@ ---- -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 deleted file mode 100644 index 0afd7fc0c..000000000 --- a/charts/orka/crds/executionworkspace-customresourcedefinition.yaml +++ /dev/null @@ -1,783 +0,0 @@ ---- -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 deleted file mode 100644 index 0b8f5632c..000000000 --- a/charts/orka/crds/executionworkspaceclass-customresourcedefinition.yaml +++ /dev/null @@ -1,333 +0,0 @@ ---- -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 deleted file mode 100644 index 66ab111ff..000000000 --- a/charts/orka/crds/executionworkspacepool-customresourcedefinition.yaml +++ /dev/null @@ -1,224 +0,0 @@ ---- -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 deleted file mode 100644 index f7e4347d3..000000000 --- a/charts/orka/crds/executionworkspaceprovider-customresourcedefinition.yaml +++ /dev/null @@ -1,292 +0,0 @@ ---- -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 deleted file mode 100644 index e17b0a44f..000000000 --- a/charts/orka/crds/externaleffect-customresourcedefinition.yaml +++ /dev/null @@ -1,191 +0,0 @@ ---- -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 deleted file mode 100644 index 3aaba45cc..000000000 --- a/charts/orka/crds/fakepoolparameters-customresourcedefinition.yaml +++ /dev/null @@ -1,51 +0,0 @@ ---- -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 deleted file mode 100644 index bc16c2a46..000000000 --- a/charts/orka/crds/fakeproviderconfig-customresourcedefinition.yaml +++ /dev/null @@ -1,51 +0,0 @@ ---- -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 deleted file mode 100644 index e51faf755..000000000 --- a/charts/orka/crds/outboundaccesspolicy-customresourcedefinition.yaml +++ /dev/null @@ -1,549 +0,0 @@ ---- -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 deleted file mode 100644 index e2781f0cc..000000000 --- a/charts/orka/crds/promptattempt-customresourcedefinition.yaml +++ /dev/null @@ -1,280 +0,0 @@ ---- -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 - bindingDigest: - description: |- - BindingDigest identifies the immutable Task-lifetime v2 execution - binding. It is optional only so pre-coexistence records remain readable; - all PromptAttempts newly created through DurableControlStore require it. - pattern: ^sha256:[a-f0-9]{64}$ - type: string - 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 - snapshotDigest: - description: |- - SnapshotDigest identifies the immutable encrypted execution snapshot. - It is optional only so pre-coexistence records remain readable; all - PromptAttempts newly created through DurableControlStore require it. - 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 - - message: bindingDigest and snapshotDigest must be recorded together - rule: has(self.bindingDigest) == has(self.snapshotDigest) - 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 deleted file mode 100644 index 0b921bca9..000000000 --- a/charts/orka/crds/publication-customresourcedefinition.yaml +++ /dev/null @@ -1,529 +0,0 @@ ---- -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/charts/orka/crds/repositorymonitor-customresourcedefinition.yaml b/charts/orka/crds/repositorymonitor-customresourcedefinition.yaml index 8dabeb7e0..ce98bfa57 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, - issue, and repair tasks. + description: Agents configures the agents used by monitor review and + repair tasks. properties: implementer: description: Implementer is the agent used for guarded issue implementation @@ -76,19 +76,6 @@ 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: @@ -102,19 +89,6 @@ 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. @@ -129,19 +103,6 @@ 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. @@ -176,28 +137,9 @@ spec: description: Branch is the default base branch for repository-wide monitoring decisions. type: string - forgeCredentialRef: - description: |- - ForgeCredentialRef references the GitHub API credential used only for - controller-owned forge reads and mutations. Write workflows and GitHub label - triggers require this explicit reference. - 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 gitSecretRef: - description: |- - GitSecretRef is the backward-compatible source-read credential reference. - ReadCredentialRef takes precedence when both are set. GitSecretRef is never - used for publication writes or forge mutations. + description: GitSecretRef references GitHub credentials for repository + monitor operations. properties: name: default: "" @@ -210,87 +152,6 @@ 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 @@ -348,57 +209,6 @@ spec: enum: - github type: string - publicationCredentialRef: - description: |- - PublicationCredentialRef references the target-repository write credential - used only for exact compare-and-swap publication. Write workflows require - this explicit reference. - 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 - publicationReadCredentialRef: - description: |- - PublicationReadCredentialRef references the target-repository read - credential used only for publication preflight and independent verification. - Write workflows require this explicit reference. - 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 - readCredentialRef: - description: |- - ReadCredentialRef references the source-repository clone/read credential. - It is resolved only by the clean-room workspace boundary. When omitted, - GitSecretRef remains the backward-compatible read-only fallback. - 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 repair: description: Repair controls bounded repair behavior. properties: @@ -579,20 +389,6 @@ 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. @@ -625,79 +421,6 @@ 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. @@ -727,11 +450,6 @@ 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. @@ -821,21 +539,11 @@ 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/repositoryscan-customresourcedefinition.yaml b/charts/orka/crds/repositoryscan-customresourcedefinition.yaml index d66b61335..776b58b50 100644 --- a/charts/orka/crds/repositoryscan-customresourcedefinition.yaml +++ b/charts/orka/crds/repositoryscan-customresourcedefinition.yaml @@ -103,32 +103,12 @@ spec: required: - name type: object - forgeCredentialRef: - description: |- - ForgeCredentialRef references the GitHub API credential used only for - controller-owned pull request reconciliation. Patch workflows require this - explicit reference. - 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 forkRepo: description: ForkRepo is the writable fork repository URL used for patch proposals. type: string gitSecretRef: - description: |- - GitSecretRef is the backward-compatible source-read credential reference. - ReadCredentialRef takes precedence when both are set. GitSecretRef is never - used for publication writes or forge mutations. + description: GitSecretRef references git credentials for private repositories. properties: name: default: "" @@ -176,57 +156,6 @@ spec: enum: - github type: string - publicationCredentialRef: - description: |- - PublicationCredentialRef references the target-repository write credential - used only for exact compare-and-swap publication. Patch workflows require - this explicit reference. - 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 - publicationReadCredentialRef: - description: |- - PublicationReadCredentialRef references the target-repository read - credential used only for publication preflight and independent verification. - Patch workflows require this explicit reference. - 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 - readCredentialRef: - description: |- - ReadCredentialRef references the source-repository clone/read credential. - It is resolved only by the clean-room workspace boundary. When omitted, - GitSecretRef remains the backward-compatible read-only fallback. - 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 ref: description: Ref is a specific git ref, tag, or commit SHA to checkout for scan tasks. diff --git a/charts/orka/crds/runtimepool-customresourcedefinition.yaml b/charts/orka/crds/runtimepool-customresourcedefinition.yaml deleted file mode 100644 index 60b385f92..000000000 --- a/charts/orka/crds/runtimepool-customresourcedefinition.yaml +++ /dev/null @@ -1,666 +0,0 @@ ---- -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 - modelLimits: - description: |- - ModelLimits pins the reviewed context and output capacities used by the - runtime's local compaction policy. - properties: - context: - description: Context is the maximum model context capacity - in tokens. - format: int64 - minimum: 1 - type: integer - output: - description: Output is the maximum generated output in - tokens. - format: int64 - minimum: 1 - type: integer - required: - - context - - output - type: object - x-kubernetes-validations: - - message: model context limit must exceed output limit - rule: self.context > self.output - 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 - - opencode - 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 - x-kubernetes-validations: - - message: OpenCode runtime profiles require modelLimits - rule: self.providerKind != 'opencode' || has(self.modelLimits) - 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 deleted file mode 100644 index eeac33fe3..000000000 --- a/charts/orka/crds/runtimesessioncontrol-customresourcedefinition.yaml +++ /dev/null @@ -1,371 +0,0 @@ ---- -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 - lineage: - description: |- - Lineage is established or verified in the same RuntimeSessionControl - status CAS that mirrors the authoritative mutation Lease. - properties: - configDigest: - description: |- - ConfigDigest freezes the configuration/execution-snapshot identity used - when the lineage was established. - pattern: ^sha256:[a-f0-9]{64}$ - type: string - contractVersion: - description: |- - AgentRuntimeContractVersion identifies the Orka-facing runtime contract. - During harness coexistence both protocol values are schema-valid; omission is - never protocol evidence and is tolerated only for stored objects awaiting the - one-time bridge classification. - enum: - - orka.harness.v1 - - orka.harness.v2 - type: string - establishedAt: - format: date-time - type: string - generation: - format: int64 - minimum: 1 - type: integer - namespaceUID: - description: |- - NamespaceUID prevents a same-name recreated namespace from attaching to - durable state owned by the previous namespace identity. - type: string - runtimeIdentity: - description: RuntimeIdentity is the built-in runtime type or AgentRuntime - UID. - maxLength: 1024 - minLength: 1 - type: string - sessionUid: - description: SessionUID repeats the immutable control identity - at the lineage fence. - maxLength: 1024 - minLength: 1 - type: string - required: - - configDigest - - contractVersion - - establishedAt - - generation - - namespaceUID - - runtimeIdentity - - sessionUid - type: object - 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)' - - message: runtime Session lineage is append-once and immutable - rule: '!has(oldSelf.lineage) || (has(self.lineage) && self.lineage == - oldSelf.lineage)' - required: - - spec - type: object - x-kubernetes-validations: - - message: runtime Session lineage UID must match the immutable control Session - UID - rule: '!has(self.status) || !has(self.status.lineage) || self.status.lineage.sessionUid - == self.spec.sessionUid' - served: true - storage: true - subresources: - status: {} diff --git a/charts/orka/crds/task-customresourcedefinition.yaml b/charts/orka/crds/task-customresourcedefinition.yaml index ece87c000..0c0354bf3 100644 --- a/charts/orka/crds/task-customresourcedefinition.yaml +++ b/charts/orka/crds/task-customresourcedefinition.yaml @@ -93,26 +93,21 @@ spec: minimum: 1 type: integer workspace: - description: |- - Workspace is the legacy harness v1 agent workspace configuration at its - historical JSON path. It is a preserved compatibility read surface for - stored v1 Tasks only: it can never be introduced or changed, and it is - not an authority surface for new work. + description: Workspace defines the working directory configuration properties: branch: - description: Branch is the git branch to checkout. + description: Branch is the git branch to checkout type: string forkRepo: description: ForkRepo is the writable fork repository URL - for pushing changes. + for pushing changes type: string gitRepo: - description: GitRepo is the repository URL to clone. + description: GitRepo is the repository URL to clone type: string gitSecretRef: - description: |- - GitSecretRef references a Secret containing git credentials. Adopted - legacy bindings freeze the exact Secret identity; new bindings reject it. + description: GitSecretRef references a Secret containing git + credentials properties: name: default: "" @@ -127,29 +122,23 @@ spec: x-kubernetes-map-type: atomic prBaseBranch: description: PRBaseBranch is the upstream branch to target - for pull requests. + for pull requests type: string pushBranch: description: |- - PushBranch is the remote branch name to push changes to after the agent - completes. + 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. + checkout type: string subPath: description: SubPath is a subdirectory within the repo to - use as workspace root. + use as workspace root type: string type: object type: object - x-kubernetes-validations: - - message: legacy agentRuntime.workspace is a preserved harness v1 - compatibility surface; new Tasks must use spec.workspace - optionalOldSelf: true - rule: '!has(self.workspace) || (oldSelf.hasValue() && has(oldSelf.value().workspace) - && self.workspace == oldSelf.value().workspace)' ai: description: AI contains AI-specific configuration (when type is "ai") properties: @@ -1378,10 +1367,10 @@ spec: type: array workspace: description: |- - 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. + 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: |- @@ -1389,18 +1378,6 @@ 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. @@ -1434,13 +1411,6 @@ 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, @@ -1509,22 +1479,7 @@ 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 @@ -1814,258 +1769,51 @@ spec: type: string workspace: description: |- - 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. + 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: - 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 source branch to check out. - maxLength: 255 + description: Branch is the git branch to checkout type: string - 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})$ + forkRepo: + description: ForkRepo is the writable fork repository URL for + pushing changes 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 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 + description: GitRepo is the repository URL to clone 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. - 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. + gitSecretRef: + description: GitSecretRef references a Secret containing git credentials 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 - 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 + default: "" 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])?)*$ + 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 - 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 + x-kubernetes-map-type: atomic + prBaseBranch: + description: PRBaseBranch is the upstream branch to target for + pull requests + type: string pushBranch: description: |- - 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 + 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 - 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 source git ref, commit SHA, or - tag to check out. - maxLength: 512 + description: Ref is a specific git ref (commit SHA, tag) to checkout 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 source repository - used as workspace root. - maxLength: 1024 + description: SubPath is a subdirectory within the repo to use + as workspace root 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 @@ -2076,219 +1824,9 @@ 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: - agentExecutionBinding: - description: |- - AgentExecutionBinding is the authoritative, write-once, immutable - execution route for this agent Task. - properties: - agent: - description: AgentExecutionAgentRef pins the exact Agent identity - resolved at binding. - properties: - generation: - format: int64 - minimum: 1 - type: integer - name: - minLength: 1 - type: string - namespace: - minLength: 1 - type: string - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - type: string - required: - - generation - - name - - namespace - - uid - type: object - backend: - description: AgentExecutionBackend identifies the isolated execution - dispatcher backend. - enum: - - harness-wrapper - - runtime-pool - - external-endpoint - type: string - bindingDigest: - description: |- - BindingDigest is the canonical digest of this binding; every durable - demand, attempt, turn, Session lease, publication, and cleanup record - copies it. - pattern: ^sha256:[a-f0-9]{64}$ - type: string - boundAt: - format: date-time - type: string - contractVersion: - description: ContractVersion is the frozen execution protocol - for the Task lifetime. - enum: - - orka.harness.v1 - - orka.harness.v2 - type: string - runtimeProfileDigest: - pattern: ^sha256:[a-f0-9]{64}$ - type: string - runtimeProfileDigestSchemaVersion: - enum: - - 1 - format: int32 - type: integer - runtimeRef: - description: AgentExecutionRuntimeRef pins a referenced AgentRuntime - identity. - properties: - generation: - format: int64 - minimum: 1 - type: integer - name: - minLength: 1 - type: string - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - type: string - required: - - generation - - name - - uid - type: object - runtimeType: - description: RuntimeType is the built-in runtime type, empty for - runtimeRef bindings. - enum: - - claude - - codex - - copilot - - opencode - type: string - schemaVersion: - enum: - - 1 - format: int32 - type: integer - snapshot: - description: AgentExecutionSnapshotRef links the immutable non-secret - execution snapshot. - properties: - digest: - pattern: ^sha256:[a-f0-9]{64}$ - type: string - id: - description: ID is the snapshot identity in the form /sha256:. - maxLength: 512 - minLength: 1 - type: string - schemaVersion: - enum: - - 1 - format: int32 - type: integer - required: - - digest - - id - - schemaVersion - type: object - task: - description: AgentExecutionBindingTaskRef pins the bound Task - identity. - properties: - boundSpecGeneration: - description: BoundSpecGeneration is the Task spec generation - frozen into the snapshot. - format: int64 - minimum: 1 - type: integer - namespaceUID: - description: |- - NamespaceUID is the UID of the Task namespace, preventing same-name - namespace recreation from satisfying old identities. - type: string - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - type: string - required: - - boundSpecGeneration - - namespaceUID - - uid - type: object - required: - - backend - - bindingDigest - - boundAt - - contractVersion - - schemaVersion - - snapshot - - task - type: object - x-kubernetes-validations: - - message: the harness-wrapper backend requires an orka.harness.v1 - binding - rule: self.backend != 'harness-wrapper' || self.contractVersion - == 'orka.harness.v1' - - message: the runtime-pool backend requires an orka.harness.v2 binding - rule: self.backend != 'runtime-pool' || self.contractVersion == - 'orka.harness.v2' attempts: description: Attempts is the number of attempts made format: int32 @@ -2311,7 +1849,6 @@ spec: enum: - Pending - Running - - Finalizing - Succeeded - Failed - Scheduled @@ -2390,519 +1927,18 @@ 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 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. @@ -2968,7 +2004,7 @@ spec: type: string type: object provider: - description: Provider is the resolved legacy workspace backend. + description: Provider is the resolved workspace backend. enum: - agent-sandbox - substrate @@ -3006,10 +2042,6 @@ 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: @@ -3022,32 +2054,13 @@ spec: It defaults to the Task namespace, or the controller namespace when configured. type: string type: object - 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 harnessRuntime: description: |- - HarnessRuntime records the controller-resolved harness v1 runtime target - for an in-flight agent turn. It intentionally stores only non-secret - routing metadata and Secret references, never bearer values. Compatibility - surface for harness v1 bindings. + 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: - attempt: - description: Attempt is the durable harness v1 attempt number. - format: int32 - minimum: 1 - type: integer authRefField: description: AuthRefField is the Secret data field selected when the turn started. @@ -3057,122 +2070,31 @@ spec: turn started. type: string authRefResourceVersion: - description: |- - AuthRefResourceVersion is the auth Secret resourceVersion validated - before starting the turn. - type: string - cancelRequestedAt: - description: |- - CancelRequestedAt records a durable cancellation request. Cancellation - remains nonterminal until a terminal frame or ledger receipt is observed. - format: date-time + 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 - controllerEpoch: - description: ControllerEpoch records the fenced controller epoch - driving the attempt. - format: int64 - minimum: 0 - type: integer endpoint: description: Endpoint is the non-secret harness base URL selected when the turn started. type: string - lastEventSeq: - description: LastEventSeq is the highest durably mapped harness - frame sequence. - format: int64 - minimum: 0 - type: integer - lastTransitionTime: - description: |- - LastTransitionTime is the last durable v1 attempt transition projected to - the Task. - format: date-time - type: string - message: - description: Message is bounded, sanitized execution context. - maxLength: 1024 - type: string - outcome: - description: Outcome is set only for a terminal harness v1 attempt. - enum: - - Succeeded - - Failed - - Cancelled - - OutcomeUnknown - type: string - reason: - description: Reason is a bounded machine-readable terminal reason - code. - maxLength: 256 - type: string - requestDigest: - description: |- - RequestDigest binds the canonical StartTurn request admitted by the - durable wrapper ledger. - pattern: ^sha256:[a-f0-9]{64}$ - 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. + 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 - runtimeSessionID: - description: RuntimeSessionID is the deterministic, non-secret - v1 runtime-session identity. - type: string - state: - description: State is the durable harness v1 attempt state projected - for operators. - enum: - - Queued - - Reserved - - SessionStarting - - Planned - - Submitting - - SubmittedUnknown - - Accepted - - Running - - Settling - - Succeeded - - Failed - - Cancelled - - OutcomeUnknown - type: string - terminalReceiptDigest: - description: TerminalReceiptDigest identifies the authoritative - terminal or unknown receipt. - pattern: ^sha256:[a-f0-9]{64}$ - type: string - turnID: - description: TurnID is the deterministic, non-secret harness turn - identity. + description: RuntimeRefName is the AgentRuntime name for custom + runtimeRef turns. Empty means built-in CLI wrapper. type: string type: object - x-kubernetes-validations: - - message: terminal harness state requires an outcome - rule: '!has(self.state) || !(self.state in [''Succeeded'', ''Failed'', - ''Cancelled'', ''OutcomeUnknown'']) || has(self.outcome)' - - message: harness outcome requires a terminal state - rule: '!has(self.outcome) || (has(self.state) && self.state in [''Succeeded'', - ''Failed'', ''Cancelled'', ''OutcomeUnknown''])' - - message: OutcomeUnknown harness state requires OutcomeUnknown outcome - rule: '!has(self.state) || self.state != ''OutcomeUnknown'' || (has(self.outcome) - && self.outcome == ''OutcomeUnknown'')' iteration: description: |- Iteration is the current autonomous loop iteration (0-based). @@ -3201,7 +2123,6 @@ spec: enum: - Pending - Running - - Finalizing - Succeeded - Failed - Scheduled @@ -3226,27 +2147,7 @@ spec: called type: boolean type: object - x-kubernetes-validations: - - message: executionOutcome is immutable once recorded - rule: '!has(oldSelf.executionOutcome) || self.executionOutcome == oldSelf.executionOutcome' - - message: agentExecutionBinding is write-once and immutable - rule: '!has(oldSelf.agentExecutionBinding) || (has(self.agentExecutionBinding) - && self.agentExecutionBinding == oldSelf.agentExecutionBinding)' - - message: a v1-bound Task cannot acquire new v2 execution or delivery - state - rule: '!has(self.agentExecutionBinding) || self.agentExecutionBinding.contractVersion - != ''orka.harness.v1'' || ((!has(self.execution) || (has(oldSelf.execution) - && self.execution == oldSelf.execution)) && (!has(self.delivery) || - (has(oldSelf.delivery) && self.delivery == oldSelf.delivery)))' - - message: a v2-bound Task cannot acquire new v1 harness state - rule: '!has(self.agentExecutionBinding) || self.agentExecutionBinding.contractVersion - != ''orka.harness.v2'' || !has(self.harnessRuntime) || (has(oldSelf.harnessRuntime) - && self.harnessRuntime == oldSelf.harnessRuntime)' type: object - x-kubernetes-validations: - - message: Task spec is immutable after execution authority is recorded - rule: '!has(oldSelf.status) || (!has(oldSelf.status.agentExecutionBinding) - || self.spec == oldSelf.spec)' served: true storage: true subresources: diff --git a/charts/orka/crds/tool-customresourcedefinition.yaml b/charts/orka/crds/tool-customresourcedefinition.yaml index 76a2338a2..3ffc6ad71 100644 --- a/charts/orka/crds/tool-customresourcedefinition.yaml +++ b/charts/orka/crds/tool-customresourcedefinition.yaml @@ -113,17 +113,6 @@ 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 @@ -145,8 +134,8 @@ spec: Defaults to /mcp. type: string substrateActor: - description: SubstrateActor configures the legacy durable Substrate - actor backend. + description: SubstrateActor configures a durable Substrate actor + that hosts the MCP server. properties: boot: description: Boot asks Substrate to boot this actor from scratch @@ -180,34 +169,9 @@ spec: required: - templateRef type: object - 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 + required: + - substrateActor 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) @@ -216,13 +180,11 @@ spec: - description type: object x-kubernetes-validations: - - 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)' + - 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: @@ -341,100 +303,6 @@ 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 8bb052154..b8c0a50b5 100644 --- a/charts/orka/templates/NOTES.txt +++ b/charts/orka/templates/NOTES.txt @@ -1,10 +1,10 @@ Orka has been installed. -Controller: {{ include "orka.controllerName" . }} +Controller: {{ include "orka.fullname" . }}-controller Namespace: {{ .Release.Namespace }} CRD lifecycle: - - A fresh install creates Orka's 26 cluster-scoped CRDs unless --skip-crds is used. + - A fresh install creates Orka's twelve 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. @@ -15,7 +15,7 @@ Upgrade guidance: helm show readme ⚠️ STORAGE WARNING: SQLite store is using ephemeral storage (emptyDir). Task results and session data will be LOST on pod restart. -{{- if and (eq .Values.controller.mode "harness-v2") .Values.controller.gateway.enabled }} +{{- if .Values.controller.gateway.enabled }} Gateway acknowledgements, deduplication, and queued deliveries are also NOT durable. {{- end }} For production use, enable persistent storage: @@ -25,7 +25,7 @@ Upgrade guidance: helm show readme {{- else }} -✅ SQLite store is using persistent storage (PVC: {{ include "orka.storeName" . }}). +✅ SQLite store is using persistent storage (PVC: {{ include "orka.fullname" . }}-store). Data will survive pod restarts. {{- end }} diff --git a/charts/orka/templates/_helpers.tpl b/charts/orka/templates/_helpers.tpl index 361e20266..7764f35bf 100644 --- a/charts/orka/templates/_helpers.tpl +++ b/charts/orka/templates/_helpers.tpl @@ -79,793 +79,30 @@ suffix so long release names cannot collapse all trust tiers to one name. {{- end }} {{/* -Create release-scoped harness v1 wrapper names while reserving room for the -longest suffix so names remain valid DNS labels for long Helm release names. +Create release-scoped harness-wrapper names while reserving room for suffixes +that must remain valid DNS labels (notably the Service name). */}} -{{- define "orka.harnessV1Name" -}} +{{- define "orka.harnessWrapperName" -}} {{- printf "%s-agent-harness-wrapper" (include "orka.fullname" . | trunc 41 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} {{- end }} -{{- define "orka.harnessV1LedgerName" -}} -{{- printf "%s-harness-v1-ledger" (include "orka.fullname" . | trunc 45 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} +{{- define "orka.harnessWrapperAuthSecretName" -}} +{{- printf "%s-harness-wrapper-auth" (include "orka.fullname" . | trunc 42 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} {{- end }} -{{- define "orka.harnessV1DrainName" -}} -{{- printf "%s-drain" (include "orka.harnessV1Name" . | trunc 57 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- define "orka.harnessV1DrainEgressName" -}} -{{- printf "%s-egress" (include "orka.harnessV1DrainName" . | trunc 56 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- define "orka.harnessV1AbortName" -}} -{{- printf "%s-abort" (include "orka.harnessV1Name" . | trunc 57 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- define "orka.harnessV1AbortEgressName" -}} -{{- printf "%s-egress" (include "orka.harnessV1AbortName" . | trunc 56 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- define "orka.harnessV1DeleteDrainName" -}} -{{- printf "%s-delete" (include "orka.harnessV1Name" . | trunc 56 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- define "orka.harnessV1DeleteDrainEgressName" -}} -{{- printf "%s-egress" (include "orka.harnessV1DeleteDrainName" . | trunc 56 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Render the complete harness v1 Pod template from one canonical helper. The -ledger generation hashes this structure with a fixed sentinel in place of the -generation itself, so only a real Pod-template change advances the generation. -*/}} -{{- define "orka.harnessV1PodTemplate" -}} -{{- $root := .root -}} -{{- $generation := .generation -}} -metadata: - labels: - {{- include "orka.labels" $root | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper - orka.ai/network-role: harness-v1 - {{- with $root.Values.harnessV1.tls.rolloutNonce }} - annotations: - orka.ai/harness-v1-tls-rollout-nonce: {{ . | quote }} - {{- end }} -spec: - serviceAccountName: {{ include "orka.harnessV1Name" $root }} - automountServiceAccountToken: false - securityContext: - runAsUser: 0 - runAsGroup: 0 - seccompProfile: - type: RuntimeDefault - containers: - - name: wrapper - image: {{ include "orka.imageRef" $root.Values.harnessV1.image | quote }} - imagePullPolicy: {{ $root.Values.harnessV1.image.pullPolicy }} - ports: - - name: https - containerPort: 8080 - protocol: TCP - env: - - name: ORKA_HARNESS_WRAPPER_RUNTIME - value: multi - - name: ORKA_HARNESS_WRAPPER_LISTEN_ADDR - value: :8080 - - name: ORKA_CONTROLLER_URL - value: http://{{ include "orka.fullname" $root }}.{{ $root.Release.Namespace }}.svc:{{ $root.Values.service.port }} - - name: ORKA_HARNESS_WRAPPER_BEARER_TOKEN_FILE - value: /var/run/orka/harness-wrapper-auth/token - - name: ORKA_HARNESS_WRAPPER_TLS_CERT_FILE - value: /var/run/orka/harness-wrapper-tls/tls.crt - - name: ORKA_HARNESS_WRAPPER_TLS_KEY_FILE - value: /var/run/orka/harness-wrapper-tls/tls.key - - name: ORKA_HARNESS_WRAPPER_ADMISSION_LEDGER_PATH - value: /var/lib/orka/harness-v1/admission-ledger.db - - name: ORKA_HARNESS_WRAPPER_LEDGER_GENERATION - value: {{ $generation | quote }} - - name: ORKA_HARNESS_WRAPPER_LEDGER_RETENTION - value: {{ $root.Values.harnessV1.ledger.retention | quote }} - - 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: {{ $root.Values.harnessV1.codexSandboxMode | quote }} - volumeMounts: - - name: auth - mountPath: /var/run/orka/harness-wrapper-auth - readOnly: true - - name: tls - mountPath: /var/run/orka/harness-wrapper-tls - readOnly: true - - name: controller-api-token - mountPath: /var/run/secrets/kubernetes.io/serviceaccount - readOnly: true - - name: ledger - mountPath: /var/lib/orka/harness-v1 - - 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: https - scheme: HTTPS - initialDelaySeconds: 10 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /v1/ready - port: https - scheme: HTTPS - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with $root.Values.harnessV1.resources }} - resources: - {{- toYaml . | nindent 8 }} - {{- end }} - volumes: - - name: auth - secret: - secretName: {{ $root.Values.harnessV1.auth.existingSecret | quote }} - defaultMode: 0400 - items: - - key: {{ $root.Values.harnessV1.auth.tokenKey | quote }} - path: token - - name: tls - secret: - secretName: {{ $root.Values.harnessV1.tls.existingSecret | quote }} - defaultMode: 0400 - items: - - key: tls.crt - path: tls.crt - - key: tls.key - path: tls.key - - key: ca.crt - path: ca.crt - - name: controller-api-token - projected: - defaultMode: 0400 - sources: - - serviceAccountToken: - path: token - expirationSeconds: 3600 - - name: ledger - persistentVolumeClaim: - claimName: {{ include "orka.harnessV1LedgerName" $root }} - - name: tmp - emptyDir: {} -{{- end }} - -{{- define "orka.harnessV1PodTemplateGeneration" -}} -{{- $template := include "orka.harnessV1PodTemplate" (dict "root" . "generation" "ORKA_HARNESS_V1_TEMPLATE_GENERATION") | fromYaml -}} -{{- toJson $template | sha256sum -}} -{{- end }} - -{{/* Read the live wrapper inputs used by rollover hooks. */}} -{{- define "orka.harnessV1ExistingImage" -}} -{{- $image := "" -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "wrapper" -}} -{{- $image = default "" .image -}} -{{- end -}} -{{- end -}} -{{- required "existing harness v1 wrapper Deployment is missing the wrapper image" $image -}} -{{- end }} - -{{- define "orka.harnessV1ExistingImagePullPolicy" -}} -{{- $pullPolicy := "IfNotPresent" -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "wrapper" -}} -{{- $pullPolicy = default "IfNotPresent" .imagePullPolicy -}} -{{- end -}} -{{- end -}} -{{- $pullPolicy -}} -{{- end }} - -{{- define "orka.harnessV1ExistingGeneration" -}} -{{- $generation := "" -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "wrapper" -}} -{{- range (default (list) .env) -}} -{{- if eq (default "" .name) "ORKA_HARNESS_WRAPPER_LEDGER_GENERATION" -}} -{{- $generation = default "" .value -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- $generation -}} -{{- end }} - -{{- define "orka.harnessV1ExistingAuthSecretName" -}} -{{- $secretName := "" -}} -{{- range (dig "spec" "template" "spec" "volumes" (list) .) -}} -{{- if eq (default "" .name) "auth" -}} -{{- $secretName = dig "secret" "secretName" "" . -}} -{{- end -}} -{{- end -}} -{{- required "existing harness v1 wrapper Deployment is missing the auth Secret name" $secretName -}} -{{- end }} - -{{- define "orka.harnessV1ExistingAuthSecretKey" -}} -{{- $secretKey := "" -}} -{{- range (dig "spec" "template" "spec" "volumes" (list) .) -}} -{{- if eq (default "" .name) "auth" -}} -{{- range (dig "secret" "items" (list) .) -}} -{{- if eq (default "" .path) "token" -}} -{{- $secretKey = default "" .key -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- required "existing harness v1 wrapper Deployment is missing the auth Secret token key" $secretKey -}} -{{- end }} - -{{- define "orka.harnessV1ExistingTLSSecretName" -}} -{{- $secretName := "" -}} -{{- $legacyAuthSecretName := "" -}} -{{- range (dig "spec" "template" "spec" "volumes" (list) .) -}} -{{- if eq (default "" .name) "tls" -}} -{{- $secretName = dig "secret" "secretName" "" . -}} -{{- else if eq (default "" .name) "auth" -}} -{{- $legacyAuthSecretName = dig "secret" "secretName" "" . -}} -{{- end -}} -{{- end -}} -{{- if not $secretName -}} -{{- $secretName = $legacyAuthSecretName -}} -{{- end -}} -{{- required "existing harness v1 wrapper Deployment is missing the TLS Secret name" $secretName -}} -{{- end }} - -{{/* Read the live controller's exact namespace watch scope. */}} -{{- define "orka.existingControllerWatchNamespace" -}} -{{- $watchNamespaces := list -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "controller" -}} -{{- range (default (list) .args) -}} -{{- $arg := toString . -}} -{{- if hasPrefix "--watch-namespace=" $arg -}} -{{- $watchNamespaces = append $watchNamespaces (trimPrefix "--watch-namespace=" $arg) -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- if eq (len $watchNamespaces) 1 -}} -{{- index $watchNamespaces 0 -}} -{{- end -}} -{{- end }} - -{{/* Read the exact chart fullname from the live controller's in-cluster URL. */}} -{{- define "orka.existingControllerFullname" -}} -{{- $fullnames := list -}} -{{- $namespaceSuffix := printf ".%s.svc" .namespace -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .controller) -}} -{{- if eq (default "" .name) "controller" -}} -{{- range (default (list) .args) -}} -{{- $arg := toString . -}} -{{- if hasPrefix "--controller-url=http://" $arg -}} -{{- $endpoint := trimPrefix "--controller-url=http://" $arg -}} -{{- $hostPort := first (splitList "/" $endpoint) -}} -{{- $host := first (splitList ":" $hostPort) -}} -{{- if hasSuffix $namespaceSuffix $host -}} -{{- $fullname := trimSuffix $namespaceSuffix $host -}} -{{- if and $fullname (not (contains "." $fullname)) -}} -{{- $fullnames = append $fullnames $fullname -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- if eq (len $fullnames) 1 -}} -{{- index $fullnames 0 -}} -{{- end -}} -{{- end }} - -{{/* Read the live controller's exact ACP runtime namespace. */}} -{{- define "orka.existingControllerACPRuntimeNamespace" -}} -{{- $runtimeNamespaces := list -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "controller" -}} -{{- range (default (list) .args) -}} -{{- $arg := toString . -}} -{{- if hasPrefix "--acp-runtime-namespace=" $arg -}} -{{- $runtimeNamespaces = append $runtimeNamespaces (trimPrefix "--acp-runtime-namespace=" $arg) -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- if eq (len $runtimeNamespaces) 1 -}} -{{- index $runtimeNamespaces 0 -}} -{{- end -}} -{{- end }} - -{{/* Read the live controller's exact static mode. Legacy controllers return empty. */}} -{{- define "orka.existingControllerMode" -}} -{{- $modes := list -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "controller" -}} -{{- range (default (list) .args) -}} -{{- $arg := toString . -}} -{{- if hasPrefix "--controller-mode=" $arg -}} -{{- $modes = append $modes (trimPrefix "--controller-mode=" $arg) -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- if eq (len $modes) 1 -}} -{{- $mode := index $modes 0 -}} -{{- if has $mode (list "harness-v1" "harness-v2") -}} -{{- $mode -}} -{{- end -}} -{{- end -}} -{{- end }} - -{{/* Read the live controller's exact agent-execution snapshot Secret name. */}} -{{- define "orka.existingControllerAgentExecutionSnapshotSecretName" -}} -{{- $volumes := list -}} -{{- range (dig "spec" "template" "spec" "volumes" (list) .) -}} -{{- if eq (default "" .name) "agent-execution-snapshot-key" -}} -{{- $volumes = append $volumes . -}} -{{- end -}} -{{- end -}} -{{- if eq (len $volumes) 1 -}} -{{- dig "secret" "secretName" "" (index $volumes 0) -}} -{{- end -}} -{{- end }} - -{{/* Read the live controller's exact snapshot Secret item mounted as key. */}} -{{- define "orka.existingControllerAgentExecutionSnapshotSecretKey" -}} -{{- $volumes := list -}} -{{- range (dig "spec" "template" "spec" "volumes" (list) .) -}} -{{- if eq (default "" .name) "agent-execution-snapshot-key" -}} -{{- $volumes = append $volumes . -}} -{{- end -}} -{{- end -}} -{{- if eq (len $volumes) 1 -}} -{{- $keys := list -}} -{{- range (dig "secret" "items" (list) (index $volumes 0)) -}} -{{- if eq (default "" .path) "key" -}} -{{- $keys = append $keys (default "" .key) -}} -{{- end -}} -{{- end -}} -{{- if eq (len $keys) 1 -}} -{{- index $keys 0 -}} -{{- end -}} -{{- end -}} -{{- end }} - -{{/* Read the live controller inputs used when the wrapper Deployment is absent. */}} -{{- define "orka.harnessV1ExistingControllerState" -}} -{{- $state := "" -}} -{{- $mode := "" -}} -{{- $harnessMarker := false -}} -{{- $harnessEnabled := false -}} -{{- $harnessDisabled := false -}} -{{- $acpEnabled := false -}} -{{- $acpDisabled := false -}} -{{- $dualMarker := false -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "controller" -}} -{{- range (default (list) .args) -}} -{{- $arg := toString . -}} -{{- if hasPrefix "--controller-mode=" $arg -}} -{{- $mode = trimPrefix "--controller-mode=" $arg -}} -{{- end -}} -{{- if hasPrefix "--harness-v1-enabled=" $arg -}} -{{- $harnessMarker = true -}} -{{- end -}} -{{- if eq $arg "--harness-v1-enabled=true" -}} -{{- $harnessEnabled = true -}} -{{- else if eq $arg "--harness-v1-enabled=false" -}} -{{- $harnessDisabled = true -}} -{{- else if eq $arg "--acp-runtime-enabled=true" -}} -{{- $acpEnabled = true -}} -{{- else if eq $arg "--acp-runtime-enabled=false" -}} -{{- $acpDisabled = true -}} -{{- else if hasPrefix "--agent-execution-" $arg -}} -{{- $dualMarker = true -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- if eq $mode "harness-v1" -}} -{{- $state = "enabled" -}} -{{- else if eq $mode "harness-v2" -}} -{{- $state = "disabled" -}} -{{- else if and $harnessEnabled (not $harnessDisabled) -}} -{{- $state = "enabled" -}} -{{- else if and $harnessDisabled (not $harnessEnabled) -}} -{{- $state = "disabled" -}} -{{- else if and (not $harnessMarker) (not $dualMarker) (ne $acpEnabled $acpDisabled) -}} -{{- $state = "legacy-v2-disabled" -}} -{{- end -}} -{{- $state -}} -{{- end }} - -{{- define "orka.harnessV1ExistingControllerAuthSecretName" -}} -{{- $secretName := "" -}} -{{- $prefix := "--harness-v1-auth-secret-name=" -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "controller" -}} -{{- range (default (list) .args) -}} -{{- $arg := toString . -}} -{{- if hasPrefix $prefix $arg -}} -{{- $secretName = trimPrefix $prefix $arg -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- required "existing harness v1 controller Deployment is missing the auth Secret name" $secretName -}} -{{- end }} - -{{- define "orka.harnessV1ExistingControllerAuthSecretKey" -}} -{{- $secretKey := "" -}} -{{- $prefix := "--harness-v1-auth-secret-key=" -}} -{{- range (dig "spec" "template" "spec" "containers" (list) .) -}} -{{- if eq (default "" .name) "controller" -}} -{{- range (default (list) .args) -}} -{{- $arg := toString . -}} -{{- if hasPrefix $prefix $arg -}} -{{- $secretKey = trimPrefix $prefix $arg -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- required "existing harness v1 controller Deployment is missing the auth Secret token key" $secretKey -}} -{{- end }} - -{{- define "orka.controllerName" -}} -{{- printf "%s-controller" (include "orka.fullname" . | trunc 52 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- define "orka.controllerWebhookServiceName" -}} -{{- printf "%s-webhook" (include "orka.fullname" . | trunc 55 | trimSuffix "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* Keep legacy short names, but hash any identity controllerName truncates. */}} -{{- define "orka.controllerWebhookName" -}} -{{- $fullname := include "orka.fullname" . -}} -{{- if le (len $fullname) 52 -}} -{{- include "orka.controllerName" . -}} -{{- else -}} -{{- $identity := printf "%s/%s/%s/%s/%s" .Release.Namespace .Release.Name (default "" .Values.fullnameOverride) (default "" .Values.nameOverride) .Chart.Name -}} -{{- printf "%s-controller-%s" ($fullname | trunc 39 | trimSuffix "-") (sha256sum $identity | trunc 12) | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end }} - -{{- define "orka.controllerClusterRoleName" -}} -{{- printf "%s-cluster" (include "orka.controllerWebhookName" .) -}} -{{- end }} - -{{- define "orka.controllerUsername" -}} -{{- printf "system:serviceaccount:%s:%s" .Release.Namespace (include "orka.serviceAccountName" .) -}} -{{- 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 the name of the workspace publisher ServiceAccount to use. -*/}} -{{- 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 }} {{/* -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 and (eq .Values.controller.mode "harness-v2") .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 }} - -{{/* -The controller uses a process-local SQLite store, so production deployments -must have exactly one elected writer and must not overlap Pods during rollout. -*/}} -{{- define "orka.validateSQLiteController" -}} -{{- if ne (int .Values.controller.replicas) 1 -}} -{{- fail "controller.replicas must be exactly 1 when using the SQLite store backend" -}} -{{- end -}} -{{- if not .Values.controller.leaderElect -}} -{{- fail "controller.leaderElect must be true when using the SQLite store backend" -}} -{{- end -}} -{{- end }} - -{{/* -Every release owns exactly one immutable execution contract and one tenant -namespace. There is no dual, automatic, or drain controller mode. -*/}} -{{- define "orka.validateControllerMode" -}} -{{- if not (has .Values.controller.mode (list "harness-v1" "harness-v2")) -}} -{{- fail "controller.mode must be harness-v1 or harness-v2" -}} -{{- end -}} -{{- if not (trim (default "" .Values.controller.watchNamespace)) -}} -{{- fail "controller.watchNamespace is required for an isolated controller installation" -}} -{{- end -}} -{{- if ne .Values.controller.watchNamespace .Release.Namespace -}} -{{- fail (printf "controller.watchNamespace must equal the Helm release namespace %q" .Release.Namespace) -}} -{{- end -}} -{{- if not .Values.controller.leaderElect -}} -{{- fail "controller.leaderElect must be true for an isolated controller installation" -}} -{{- end -}} -{{- if .Release.IsUpgrade -}} -{{- $existingNamespace := lookup "v1" "Namespace" "" .Release.Namespace -}} -{{- $existingNamespaceMode := "" -}} -{{- if $existingNamespace -}} -{{- $existingNamespaceMode = dig "metadata" "labels" "orka.ai/controller-mode" "" $existingNamespace -}} -{{- end -}} -{{- if ne $existingNamespaceMode .Values.controller.mode -}} -{{- fail (printf "controller mode identity is missing or incompatible; namespace %q must already claim orka.ai/controller-mode=%s before this release can be upgraded" .Release.Namespace .Values.controller.mode) -}} -{{- end -}} -{{- $root := . -}} -{{- $existingControllerList := lookup "apps/v1" "Deployment" .Release.Namespace "" -}} -{{- $existingControllers := list -}} -{{- range (dig "items" (list) (default (dict) $existingControllerList)) -}} -{{- $labels := dig "metadata" "labels" (dict) . -}} -{{- if and (eq (get $labels "app.kubernetes.io/instance") $root.Release.Name) (eq (get $labels "app.kubernetes.io/component") "controller") (eq (get $labels "app.kubernetes.io/managed-by") $root.Release.Service) -}} -{{- $existingControllers = append $existingControllers . -}} -{{- end -}} -{{- end -}} -{{- if gt (len $existingControllers) 1 -}} -{{- fail (printf "multiple controller Deployments are owned by Helm release %q in namespace %q; restore a single controller before upgrading" .Release.Name .Release.Namespace) -}} -{{- end -}} -{{- $existingController := dict -}} -{{- if eq (len $existingControllers) 1 -}} -{{- $existingController = index $existingControllers 0 -}} -{{- end -}} -{{- if $existingController -}} -{{- $existingWatchNamespace := include "orka.existingControllerWatchNamespace" $existingController | trim -}} -{{- if ne $existingWatchNamespace .Values.controller.watchNamespace -}} -{{- fail (printf "controller.watchNamespace is immutable; the existing controller must already watch namespace %q; install cluster-wide or differently scoped controllers as a new release and namespace" .Values.controller.watchNamespace) -}} -{{- end -}} -{{- $existingMode := include "orka.existingControllerMode" $existingController | trim -}} -{{- $existingState := include "orka.harnessV1ExistingControllerState" $existingController | trim -}} -{{- if $existingMode -}} -{{- if ne $existingMode .Values.controller.mode -}} -{{- fail (printf "controller.mode is immutable; install %s as a new release and namespace" .Values.controller.mode) -}} -{{- end -}} -{{- else if eq .Values.controller.mode "harness-v2" -}} -{{- fail "implicit or legacy harness-v2 installations cannot upgrade in place; settle or retire the existing installation and install harness-v2 as a new release and namespace" -}} -{{- else if ne $existingState "enabled" -}} -{{- fail "controller.mode is immutable; install harness-v1 as a new release and namespace" -}} -{{- end -}} -{{- $existingSnapshotSecret := include "orka.existingControllerAgentExecutionSnapshotSecretName" $existingController | trim -}} -{{- if not $existingSnapshotSecret -}} -{{- fail "cannot determine the existing agent execution snapshot Secret name from the live controller; restore its exact agent-execution-snapshot-key volume before upgrading" -}} -{{- end -}} -{{- $desiredSnapshotSecret := trim (default "" .Values.controller.agentExecutionSnapshot.existingSecret) -}} -{{- if ne $existingSnapshotSecret $desiredSnapshotSecret -}} -{{- fail (printf "controller.agentExecutionSnapshot.existingSecret is immutable for in-place upgrades; preserve %q so retained encrypted execution snapshots remain decryptable" $existingSnapshotSecret) -}} -{{- end -}} -{{- $existingSnapshotKey := include "orka.existingControllerAgentExecutionSnapshotSecretKey" $existingController | trim -}} -{{- if not $existingSnapshotKey -}} -{{- fail "cannot determine the existing agent execution snapshot Secret key from the live controller; restore its exact item mounted at path key before upgrading" -}} -{{- end -}} -{{- $desiredSnapshotKey := trim (default "" .Values.controller.agentExecutionSnapshot.key) -}} -{{- if ne $existingSnapshotKey $desiredSnapshotKey -}} -{{- fail (printf "controller.agentExecutionSnapshot.key is immutable for in-place upgrades; preserve %q so retained encrypted execution snapshots remain decryptable" $existingSnapshotKey) -}} -{{- end -}} -{{- if eq .Values.controller.mode "harness-v2" -}} -{{- $existingFullname := include "orka.existingControllerFullname" (dict "controller" $existingController "namespace" .Release.Namespace) | trim -}} -{{- if not $existingFullname -}} -{{- fail "cannot determine the existing harness-v2 chart fullname from the live controller; restore its exact --controller-url argument before upgrading" -}} -{{- end -}} -{{- $desiredFullname := include "orka.fullname" . -}} -{{- if ne $existingFullname $desiredFullname -}} -{{- fail (printf "the effective chart fullname is immutable for harness-v2 upgrades; the existing controller uses %q, but this upgrade would use %q" $existingFullname $desiredFullname) -}} -{{- end -}} -{{- $existingRuntimeNamespace := include "orka.existingControllerACPRuntimeNamespace" $existingController | trim -}} -{{- if not $existingRuntimeNamespace -}} -{{- fail "cannot determine the existing harness-v2 ACP runtime namespace; restore its exact --acp-runtime-namespace argument before upgrading" -}} -{{- end -}} -{{- if ne $existingRuntimeNamespace .Values.controller.acpRuntime.namespace -}} -{{- fail (printf "controller.acpRuntime.namespace is immutable; the existing controller uses namespace %q" $existingRuntimeNamespace) -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- end -}} -{{- $clientNamespace := trim (default "" .Values.client.namespace) -}} -{{- if and $clientNamespace (ne $clientNamespace .Values.controller.watchNamespace) -}} -{{- fail "client.namespace must be empty or match controller.watchNamespace" -}} -{{- end -}} -{{- if eq .Values.controller.mode "harness-v2" -}} -{{- if not (trim (default "" .Values.controller.acpRuntime.namespace)) -}} -{{- fail "controller.acpRuntime.namespace is required when controller.mode=harness-v2" -}} -{{- end -}} -{{- if eq .Values.controller.acpRuntime.namespace .Release.Namespace -}} -{{- fail "controller.acpRuntime.namespace must differ from the release namespace" -}} -{{- end -}} -{{- end -}} -{{- end }} - -{{/* -Agent execution snapshots contain sensitive resolved inputs. When either -agent protocol is enabled, require an operator-managed Secret for their -encryption key rather than generating or storing the key in Helm values. -*/}} -{{- define "orka.validateAgentExecutionSnapshot" -}} -{{- if not (trim (default "" .Values.controller.agentExecutionSnapshot.existingSecret)) -}} -{{- fail "controller.agentExecutionSnapshot.existingSecret is required when agent execution is enabled" -}} -{{- end -}} -{{- if not (trim (default "" .Values.controller.agentExecutionSnapshot.key)) -}} -{{- fail "controller.agentExecutionSnapshot.key is required when agent execution is enabled" -}} -{{- end -}} -{{- end }} - -{{/* -The release-local controller serves its own fail-closed webhooks. Its -certificate and CA trust are always operator-managed. -*/}} -{{- define "orka.validateWebhooks" -}} -{{- if not (trim (default "" .Values.webhooks.tls.existingSecret)) -}} -{{- fail "webhooks.tls.existingSecret is required" -}} -{{- end -}} -{{- if not (trim (default "" .Values.webhooks.tls.certKey)) -}} -{{- fail "webhooks.tls.certKey is required" -}} -{{- end -}} -{{- if not (trim (default "" .Values.webhooks.tls.privateKeyKey)) -}} -{{- fail "webhooks.tls.privateKeyKey is required" -}} -{{- end -}} -{{- if and (not (trim (default "" .Values.webhooks.caBundle))) (empty .Values.webhooks.caInjectionAnnotations) -}} -{{- fail "webhooks requires a nonempty caBundle or caInjectionAnnotations" -}} -{{- end -}} -{{- if or (lt (int .Values.webhooks.timeoutSeconds) 1) (gt (int .Values.webhooks.timeoutSeconds) 30) -}} -{{- fail "webhooks.timeoutSeconds must be between 1 and 30" -}} -{{- end -}} -{{- end }} - -{{/* -Harness v1 is an explicitly selected compatibility data plane. Its image must -be immutable, its admission ledger durable, and its bearer credential must -remain outside rendered Helm manifests. -*/}} -{{- define "orka.validateHarnessV1" -}} -{{- if eq .Values.controller.mode "harness-v1" -}} -{{- if .Values.controller.agentSandbox.enabled -}} -{{- fail "controller.agentSandbox.enabled is unsupported when controller.mode=harness-v1; Agent Sandbox requires harness-v2" -}} -{{- end -}} -{{- if .Values.controller.substrate.enabled -}} -{{- fail "controller.substrate.enabled is unsupported when controller.mode=harness-v1; Substrate requires harness-v2" -}} -{{- end -}} -{{- if not (trim (default "" .Values.harnessV1.image.repository)) -}} -{{- fail "harnessV1.image.repository is required when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (regexMatch "^sha256:[0-9a-f]{64}$" (.Values.harnessV1.image.digest | default "")) -}} -{{- fail "harnessV1.image.digest must be a sha256 digest when controller.mode=harness-v1" -}} -{{- end -}} -{{- if trim (default "" .Values.harnessV1.auth.token) -}} -{{- fail "harnessV1.auth.token is unsupported; create a Kubernetes Secret and set harnessV1.auth.existingSecret" -}} -{{- end -}} -{{- if not (trim (default "" .Values.harnessV1.auth.existingSecret)) -}} -{{- fail "harnessV1.auth.existingSecret is required when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (trim (default "" .Values.harnessV1.auth.tokenKey)) -}} -{{- fail "harnessV1.auth.tokenKey is required when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (trim (default "" .Values.harnessV1.tls.existingSecret)) -}} -{{- fail "harnessV1.tls.existingSecret is required when controller.mode=harness-v1" -}} -{{- end -}} -{{- if eq (trim .Values.harnessV1.auth.existingSecret) (trim .Values.harnessV1.tls.existingSecret) -}} -{{- fail "harnessV1.tls.existingSecret must differ from harnessV1.auth.existingSecret" -}} -{{- end -}} -{{- if not (trim (default "" .Values.harnessV1.ledger.size)) -}} -{{- fail "harnessV1.ledger.size is required when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (trim (default "" .Values.harnessV1.ledger.retention)) -}} -{{- fail "harnessV1.ledger.retention is required when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (regexMatch "^([1-9][0-9]*(ns|us|µs|ms|s|m|h))+$" (trim (default "" .Values.harnessV1.ledger.retention))) -}} -{{- fail "harnessV1.ledger.retention must be a positive Go duration when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not .Values.store.persistence.enabled -}} -{{- fail "store.persistence.enabled must be true when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (trim (default "" .Values.harnessV1.dispatch.interval)) -}} -{{- fail "harnessV1.dispatch.interval is required when controller.mode=harness-v1" -}} -{{- end -}} -{{- if ne (int .Values.harnessV1.dispatch.workers) 1 -}} -{{- fail "harnessV1.dispatch.workers must be exactly 1 when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (regexMatch "^([1-9][0-9]*(ns|us|µs|ms|s|m|h))+$" (trim (default "" .Values.harnessV1.upgradeDrain.timeout))) -}} -{{- fail "harnessV1.upgradeDrain.timeout must be a positive Go duration when controller.mode=harness-v1" -}} -{{- end -}} -{{- if not (regexMatch "^([1-9][0-9]*(ns|us|µs|ms|s|m|h))+$" (trim (default "" .Values.harnessV1.upgradeDrain.pollInterval))) -}} -{{- fail "harnessV1.upgradeDrain.pollInterval must be a positive Go duration when controller.mode=harness-v1" -}} -{{- end -}} -{{- $sandboxMode := trim (default "" .Values.harnessV1.codexSandboxMode) -}} -{{- if and $sandboxMode (not (has $sandboxMode (list "read-only" "workspace-write" "danger-full-access"))) -}} -{{- fail "harnessV1.codexSandboxMode must be read-only, workspace-write, or danger-full-access" -}} -{{- end -}} -{{- end -}} -{{- end }} - -{{- define "orka.providerProxyUpstreamBaseURL" -}} -{{- trimSuffix "/" (trim (default "" .Values.providerProxy.upstreamBaseURL)) -}} -{{- end }} - - -{{/* -Create the namespace for the chart-managed client ServiceAccount. Static -installations always place the client in the watched namespace. +Create the namespace for the chart-managed client ServiceAccount. +When namespace isolation is enforced and the controller watches one namespace, +place the default client in that namespace so its token remains usable. */}} {{- define "orka.clientNamespace" -}} {{- if .Values.client.namespace }} {{- .Values.client.namespace }} -{{- else }} +{{- else if and .Values.controller.enforceNamespaceIsolation .Values.controller.watchNamespace }} {{- .Values.controller.watchNamespace }} +{{- else }} +{{- .Release.Namespace }} {{- end }} {{- end }} @@ -885,25 +122,16 @@ Create release-scoped worker ClusterRole names. {{- end }} {{/* -Create release-scoped static worker RoleBinding names. +Create release-scoped static worker ClusterRoleBinding names. */}} -{{- define "orka.aiWorkerRoleBindingName" -}} +{{- define "orka.aiWorkerClusterRoleBindingName" -}} {{- printf "%s-ai-worker-rolebinding" (include "orka.fullname" .) | trunc 253 | trimSuffix "-" }} {{- end }} -{{- define "orka.vendorWorkerRoleBindingName" -}} +{{- define "orka.vendorWorkerClusterRoleBindingName" -}} {{- printf "%s-vendor-worker-rolebinding" (include "orka.fullname" .) | trunc 253 | trimSuffix "-" }} {{- end }} -{{- define "orka.containerWorkerRoleBindingName" -}} +{{- 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 deleted file mode 100644 index 49343eb1a..000000000 --- a/charts/orka/templates/acp-artifact-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") (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/admission-deployment.yaml b/charts/orka/templates/admission-deployment.yaml deleted file mode 100644 index 1d81ba4fd..000000000 --- a/charts/orka/templates/admission-deployment.yaml +++ /dev/null @@ -1,4 +0,0 @@ -{{/* -The dedicated chart deployment was retired. Static releases serve their -mode-scoped validation endpoints from the release-local controller. -*/}} diff --git a/charts/orka/templates/controller-validating-webhook.yaml b/charts/orka/templates/controller-validating-webhook.yaml deleted file mode 100644 index 13472d206..000000000 --- a/charts/orka/templates/controller-validating-webhook.yaml +++ /dev/null @@ -1,191 +0,0 @@ -{{- include "orka.validateControllerMode" . }} -{{- include "orka.validateWebhooks" . }} -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingWebhookConfiguration -metadata: - name: {{ include "orka.controllerWebhookName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: controller - {{- with .Values.webhooks.caInjectionAnnotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -webhooks: - - name: namespace-mode.{{ .Values.controller.mode }}.orka.ai - admissionReviewVersions: [v1] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: {{ .Values.webhooks.timeoutSeconds }} - clientConfig: - service: - name: {{ include "orka.controllerWebhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-v1-namespace-execution-mode - port: 443 - {{- with .Values.webhooks.caBundle }} - caBundle: {{ . | quote }} - {{- end }} - rules: - - operations: [CREATE, UPDATE] - apiGroups: [""] - apiVersions: [v1] - resources: [namespaces] - scope: Cluster - objectSelector: - matchLabels: - orka.ai/controller-mode: {{ .Values.controller.mode | quote }} - kubernetes.io/metadata.name: {{ .Release.Namespace | quote }} - - name: task-provenance.{{ .Values.controller.mode }}.orka.ai - admissionReviewVersions: [v1] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: {{ .Values.webhooks.timeoutSeconds }} - clientConfig: - service: - name: {{ include "orka.controllerWebhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-task-provenance - port: 443 - {{- with .Values.webhooks.caBundle }} - caBundle: {{ . | quote }} - {{- end }} - rules: - - operations: [CREATE, UPDATE] - apiGroups: [core.orka.ai] - apiVersions: [v1alpha1] - resources: [tasks] - scope: Namespaced - namespaceSelector: - matchLabels: - orka.ai/controller-mode: {{ .Values.controller.mode | quote }} - kubernetes.io/metadata.name: {{ .Release.Namespace | quote }} - {{- if eq .Values.controller.mode "harness-v2" }} - - name: task-workspace-class.{{ .Values.controller.mode }}.orka.ai - admissionReviewVersions: [v1] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: {{ .Values.webhooks.timeoutSeconds }} - clientConfig: - service: - name: {{ include "orka.controllerWebhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-task-workspace-class-use - port: 443 - {{- with .Values.webhooks.caBundle }} - caBundle: {{ . | quote }} - {{- end }} - rules: - - operations: [CREATE, UPDATE] - apiGroups: [core.orka.ai] - apiVersions: [v1alpha1] - resources: [tasks] - scope: Namespaced - namespaceSelector: - matchLabels: - orka.ai/controller-mode: {{ .Values.controller.mode | quote }} - kubernetes.io/metadata.name: {{ .Release.Namespace | quote }} - - name: tool-workspace-class.{{ .Values.controller.mode }}.orka.ai - admissionReviewVersions: [v1] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: {{ .Values.webhooks.timeoutSeconds }} - clientConfig: - service: - name: {{ include "orka.controllerWebhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-tool-workspace-class-use - port: 443 - {{- with .Values.webhooks.caBundle }} - caBundle: {{ . | quote }} - {{- end }} - rules: - - operations: [CREATE, UPDATE] - apiGroups: [core.orka.ai] - apiVersions: [v1alpha1] - resources: [tools] - scope: Namespaced - namespaceSelector: - matchLabels: - orka.ai/controller-mode: {{ .Values.controller.mode | quote }} - kubernetes.io/metadata.name: {{ .Release.Namespace | quote }} - {{- end }} - - name: agent-contract.{{ .Values.controller.mode }}.orka.ai - admissionReviewVersions: [v1] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: {{ .Values.webhooks.timeoutSeconds }} - clientConfig: - service: - name: {{ include "orka.controllerWebhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-agent-contract - port: 443 - {{- with .Values.webhooks.caBundle }} - caBundle: {{ . | quote }} - {{- end }} - rules: - - operations: [CREATE, UPDATE] - apiGroups: [core.orka.ai] - apiVersions: [v1alpha1] - resources: [agents] - scope: Namespaced - namespaceSelector: - matchLabels: - orka.ai/controller-mode: {{ .Values.controller.mode | quote }} - kubernetes.io/metadata.name: {{ .Release.Namespace | quote }} - - name: agentruntime-contract.{{ .Values.controller.mode }}.orka.ai - admissionReviewVersions: [v1] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: {{ .Values.webhooks.timeoutSeconds }} - clientConfig: - service: - name: {{ include "orka.controllerWebhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-agentruntime-contract - port: 443 - {{- with .Values.webhooks.caBundle }} - caBundle: {{ . | quote }} - {{- end }} - rules: - - operations: [CREATE, UPDATE] - apiGroups: [core.orka.ai] - apiVersions: [v1alpha1] - resources: [agentruntimes] - scope: Namespaced - namespaceSelector: - matchLabels: - orka.ai/controller-mode: {{ .Values.controller.mode | quote }} - kubernetes.io/metadata.name: {{ .Release.Namespace | quote }} - - name: task-execution-authority.{{ .Values.controller.mode }}.orka.ai - admissionReviewVersions: [v1] - sideEffects: None - failurePolicy: Fail - matchPolicy: Equivalent - timeoutSeconds: {{ .Values.webhooks.timeoutSeconds }} - clientConfig: - service: - name: {{ include "orka.controllerWebhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-core-orka-ai-v1alpha1-task-execution-authority - port: 443 - {{- with .Values.webhooks.caBundle }} - caBundle: {{ . | quote }} - {{- end }} - rules: - - operations: [CREATE, UPDATE] - apiGroups: [core.orka.ai] - apiVersions: [v1alpha1] - resources: [tasks, tasks/status] - scope: Namespaced - namespaceSelector: - matchLabels: - orka.ai/controller-mode: {{ .Values.controller.mode | quote }} - kubernetes.io/metadata.name: {{ .Release.Namespace | quote }} diff --git a/charts/orka/templates/controller-webhook-service.yaml b/charts/orka/templates/controller-webhook-service.yaml deleted file mode 100644 index 19f96695e..000000000 --- a/charts/orka/templates/controller-webhook-service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "orka.controllerWebhookServiceName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: controller -spec: - type: ClusterIP - ports: - - port: 443 - targetPort: webhook - protocol: TCP - name: webhook - selector: - {{- include "orka.selectorLabels" . | nindent 4 }} - app.kubernetes.io/component: controller diff --git a/charts/orka/templates/deployment.yaml b/charts/orka/templates/deployment.yaml index 750316f89..13e213d59 100644 --- a/charts/orka/templates/deployment.yaml +++ b/charts/orka/templates/deployment.yaml @@ -1,33 +1,5 @@ -{{- $harnessV1 := eq .Values.controller.mode "harness-v1" -}} -{{- $harnessV2 := eq .Values.controller.mode "harness-v2" -}} -{{- include "orka.validateControllerMode" . }} -{{- include "orka.validateProviderProxyConfig" . }} -{{- include "orka.validateSQLiteController" . }} -{{- include "orka.validateAgentExecutionSnapshot" . }} -{{- include "orka.validateWebhooks" . }} -{{- if not (regexMatch "^sha256:[0-9a-f]{64}$" (.Values.controller.image.digest | default "")) }} -{{- fail "controller.image.digest must be a sha256 digest" }} -{{- end }} -{{- if $harnessV2 }} -{{- if not (regexMatch "^sha256:[0-9a-f]{64}$" (.Values.controller.image.digest | default "")) }} -{{- fail "controller.image.digest must be a sha256 digest when controller.mode=harness-v2" }} -{{- 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.mode=harness-v2" }} -{{- end }} -{{- if not .Values.store.persistence.enabled }} -{{- fail "store.persistence.enabled must be true when controller.mode=harness-v2" }} -{{- 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) }} -{{- include "orka.validateACPRuntimeImage" (dict "name" "controller.acpRuntime.opencodeImage" "ref" .Values.controller.acpRuntime.opencodeImage) }} -{{- if not .Values.providerProxy.enabled }} -{{- fail "providerProxy.enabled must be true when controller.mode=harness-v2" }} -{{- end }} -{{- end }} {{- $gatewayCRDsReady := false -}} -{{- if and $harnessV2 .Values.controller.gateway.enabled (not .Values.controller.gateway.crdsReadyOverride) -}} +{{- if and .Values.controller.gateway.enabled (not .Values.controller.gateway.crdsReadyOverride) -}} {{- $gatewayClassCRD := lookup "apiextensions.k8s.io/v1" "CustomResourceDefinition" "" "gatewayclasses.gateway.orka.ai" -}} {{- $gatewayCRD := lookup "apiextensions.k8s.io/v1" "CustomResourceDefinition" "" "gateways.gateway.orka.ai" -}} {{- $gatewayBindingCRD := lookup "apiextensions.k8s.io/v1" "CustomResourceDefinition" "" "gatewaybindings.gateway.orka.ai" -}} @@ -38,21 +10,23 @@ {{- end -}} {{- $gatewayCRDsReady = and (not (empty $gatewayClassCRD)) (not (empty $gatewayCRD)) (not (empty $gatewayBindingCRD)) $taskSchemaReady -}} {{- end -}} -{{- $gatewayRuntimeEnabled := and $harnessV2 .Values.controller.gateway.enabled (or .Values.controller.gateway.crdsReadyOverride $gatewayCRDsReady) -}} +{{- $gatewayRuntimeEnabled := and .Values.controller.gateway.enabled (or .Values.controller.gateway.crdsReadyOverride $gatewayCRDsReady) -}} {{- if and $gatewayRuntimeEnabled (not .Values.store.persistence.enabled) (not .Values.controller.gateway.allowEphemeralStore) -}} {{- fail "controller.gateway.enabled requires store.persistence.enabled=true; set controller.gateway.allowEphemeralStore=true only for disposable development" -}} {{- end -}} apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "orka.controllerName" . }} + name: {{ include "orka.fullname" . }}-controller labels: {{- include "orka.labels" . | nindent 4 }} app.kubernetes.io/component: controller spec: replicas: {{ .Values.controller.replicas }} + {{- if .Values.store.persistence.enabled }} strategy: type: Recreate + {{- end }} selector: matchLabels: {{- include "orka.selectorLabels" . | nindent 6 }} @@ -62,18 +36,9 @@ spec: labels: {{- include "orka.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: controller - orka.ai/network-role: controller - {{- if or .Values.annotations (and $harnessV2 .Values.publisher.auth.rolloutNonce) (and $harnessV1 .Values.harnessV1.tls.rolloutNonce) }} + {{- with .Values.annotations }} annotations: - {{- with .Values.annotations }} {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.publisher.auth.rolloutNonce }} - orka.ai/publisher-auth-rollout-nonce: {{ . | quote }} - {{- end }} - {{- with .Values.harnessV1.tls.rolloutNonce }} - orka.ai/harness-v1-tls-rollout-nonce: {{ . | quote }} - {{- end }} {{- end }} spec: serviceAccountName: {{ include "orka.serviceAccountName" . }} @@ -81,12 +46,9 @@ spec: securityContext: {{- toYaml . | nindent 8 }} {{- end }} - {{- if and $harnessV2 .Values.controller.acpRuntime.upgradeDrain.enabled }} - terminationGracePeriodSeconds: {{ .Values.controller.acpRuntime.upgradeDrain.terminationGracePeriodSeconds }} - {{- end }} containers: - name: controller - image: {{ include "orka.imageRef" .Values.controller.image | quote }} + image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag }}" imagePullPolicy: {{ .Values.controller.image.pullPolicy }} args: - --api-port={{ .Values.controller.apiPort }} @@ -94,56 +56,10 @@ spec: - --metrics-bind-address=:{{ .Values.controller.metricsPort }} - --health-probe-bind-address=:{{ .Values.controller.healthPort }} - --metrics-secure=false + {{- if .Values.controller.watchNamespace }} - --watch-namespace={{ .Values.controller.watchNamespace }} - - --leader-elect={{ .Values.controller.leaderElect }} - - --controller-mode={{ .Values.controller.mode }} - - {{ printf "--execution-mode-controller-usernames=%s" (include "orka.controllerUsername" .) | quote }} - - --webhook-cert-path=/var/run/orka/webhook/tls - - {{ printf "--webhook-cert-name=%s" .Values.webhooks.tls.certKey | quote }} - - {{ printf "--webhook-cert-key=%s" .Values.webhooks.tls.privateKeyKey | quote }} - - --task-provenance-admission-enabled=true - - {{ printf "--task-provenance-admission-trusted-users=%s" (include "orka.controllerUsername" .) | quote }} - - --workspace-class-use-admission-enabled={{ $harnessV2 }} - {{- if $harnessV1 }} - - --harness-v1-endpoint=https://{{ include "orka.harnessV1Name" . }}.{{ .Release.Namespace }}.svc:8080 - - --harness-v1-ca-file=/var/run/orka/harness-v1-tls/ca.crt - - --harness-v1-auth-secret-namespace={{ .Release.Namespace }} - - --harness-v1-auth-secret-name={{ .Values.harnessV1.auth.existingSecret }} - - --harness-v1-auth-secret-key={{ .Values.harnessV1.auth.tokenKey }} - - --harness-v1-dispatch-interval={{ .Values.harnessV1.dispatch.interval }} - - --harness-v1-dispatch-workers={{ .Values.harnessV1.dispatch.workers }} - {{- end }} - - --agent-execution-snapshot-key-file=/var/run/orka/agent-execution-snapshot/key - - --agent-execution-snapshot-retention={{ .Values.controller.agentExecutionSnapshot.retention }} - - --agent-execution-snapshot-retention-interval={{ .Values.controller.agentExecutionSnapshot.retentionInterval }} - {{- if $harnessV2 }} - - --acp-runtime-namespace={{ .Values.controller.acpRuntime.namespace }} - {{- end }} - {{- if and $harnessV2 .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 $harnessV2 .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 }} - {{- if .Values.controller.acpRuntime.opencodeImage }} - - {{ printf "--acp-opencode-runtime-image=%s" .Values.controller.acpRuntime.opencodeImage | quote }} {{- end }} + - --leader-elect={{ .Values.controller.leaderElect }} - --zap-log-level={{ .Values.controller.logLevel }} - --gateway-enabled={{ $gatewayRuntimeEnabled }} - --gateway-pending-per-session={{ .Values.controller.gateway.pendingPerSession }} @@ -268,7 +184,7 @@ spec: {{- end }} {{- with .tts }} {{- if .url }} - - {{ printf "--context-token-tts-endpoint=%s" .url | quote }} + - {{ printf "--context-token-tts-url=%s" .url | quote }} {{- end }} {{- if .audience }} - {{ printf "--context-token-tts-audience=%s" .audience | quote }} @@ -306,8 +222,10 @@ spec: - --ai-worker-cluster-role-name={{ include "orka.aiWorkerClusterRoleName" . }} - --vendor-worker-cluster-role-name={{ include "orka.vendorWorkerClusterRoleName" . }} - --container-worker-cluster-role-name={{ include "orka.containerWorkerClusterRoleName" . }} - - --worker-role-binding-prefix={{ include "orka.fullname" . }} + - --worker-cluster-role-binding-prefix={{ include "orka.fullname" . }} + {{- if .Values.controller.enforceNamespaceIsolation }} - --enforce-namespace-isolation=true + {{- end }} {{- if gt (int .Values.controller.maxTasksPerNamespace) 0 }} - --max-tasks-per-namespace={{ .Values.controller.maxTasksPerNamespace }} {{- end }} @@ -364,22 +282,12 @@ spec: value: {{ include "orka.vendorWorkerClusterRoleName" . | quote }} - name: ORKA_CONTAINER_WORKER_CLUSTER_ROLE_NAME value: {{ include "orka.containerWorkerClusterRoleName" . | quote }} - {{- if $harnessV2 }} - - 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 }} - {{- end }} - {{- if and $harnessV2 .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 }} + - 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 }} {{- if .Values.github.webhook.secretName }} - name: ORKA_GITHUB_WEBHOOK_SECRET valueFrom: @@ -427,18 +335,6 @@ spec: - name: health containerPort: {{ .Values.controller.healthPort }} protocol: TCP - - name: webhook - containerPort: 9443 - protocol: TCP - {{- if and $harnessV2 .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 @@ -464,96 +360,25 @@ spec: mountPath: /tmp - name: store mountPath: /data - {{- if $harnessV2 }} - - name: acp-artifact-capability - mountPath: /var/run/orka/acp-artifacts - readOnly: true - {{- end }} - {{- if and $harnessV2 .Values.publisher.enabled }} - - name: workspace-publisher-auth - mountPath: /var/run/orka/publisher-auth - readOnly: true - {{- end }} - {{- if and $harnessV2 .Values.providerProxy.enabled }} - - name: provider-auth-proxy - mountPath: /var/run/orka/provider-auth - readOnly: true - {{- end }} - - name: agent-execution-snapshot-key - mountPath: /var/run/orka/agent-execution-snapshot - readOnly: true - - name: webhook-tls - mountPath: /var/run/orka/webhook/tls + - name: harness-wrapper-auth + mountPath: /var/run/orka/harness-wrapper readOnly: true - {{- if $harnessV1 }} - - name: harness-v1-tls - mountPath: /var/run/orka/harness-v1-tls - readOnly: true - {{- end }} volumes: - name: tmp emptyDir: {} - {{- if $harnessV2 }} - - name: acp-artifact-capability + - name: harness-wrapper-auth secret: - secretName: {{ .Values.controller.acpArtifact.existingSecret | default (include "orka.acpArtifactSecretName" .) }} - defaultMode: 0400 + secretName: {{ .Values.workers.harnessWrapper.auth.existingSecret | default (include "orka.harnessWrapperAuthSecretName" .) }} items: - - key: {{ .Values.controller.acpArtifact.secretKey | default "capability-secret" }} - path: capability-secret - {{- end }} + - key: {{ .Values.workers.harnessWrapper.auth.tokenKey | default "token" }} + path: token - name: store {{- if .Values.store.persistence.enabled }} persistentVolumeClaim: - claimName: {{ include "orka.storeName" . }} + claimName: {{ include "orka.fullname" . }}-store {{- else }} emptyDir: {} {{- end }} - {{- if and $harnessV2 .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 $harnessV2 .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 }} - - name: agent-execution-snapshot-key - secret: - secretName: {{ .Values.controller.agentExecutionSnapshot.existingSecret | quote }} - defaultMode: 0400 - items: - - key: {{ .Values.controller.agentExecutionSnapshot.key | quote }} - path: key - - name: webhook-tls - secret: - secretName: {{ .Values.webhooks.tls.existingSecret | quote }} - defaultMode: 0400 - items: - - key: {{ .Values.webhooks.tls.certKey | quote }} - path: {{ .Values.webhooks.tls.certKey | quote }} - - key: {{ .Values.webhooks.tls.privateKeyKey | quote }} - path: {{ .Values.webhooks.tls.privateKeyKey | quote }} - {{- if $harnessV1 }} - - name: harness-v1-tls - secret: - secretName: {{ .Values.harnessV1.tls.existingSecret | quote }} - defaultMode: 0400 - items: - - key: ca.crt - path: ca.crt - {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/orka/templates/gateway-task-admission-policy.yaml b/charts/orka/templates/gateway-task-admission-policy.yaml index 940409346..12a1b1976 100644 --- a/charts/orka/templates/gateway-task-admission-policy.yaml +++ b/charts/orka/templates/gateway-task-admission-policy.yaml @@ -1,4 +1,3 @@ -{{- if eq .Values.controller.mode "harness-v2" }} {{- $issuerPrefix := "gateway.orka.ai/" -}} {{- if .Values.controller.watchNamespace -}} {{- $issuerPrefix = printf "gateway.orka.ai/%s/" .Values.controller.watchNamespace -}} @@ -44,4 +43,3 @@ metadata: spec: policyName: {{ include "orka.fullname" . }}-gateway-task-protection validationActions: [Deny] -{{- end }} diff --git a/charts/orka/templates/harness-wrapper-deployment.yaml b/charts/orka/templates/harness-wrapper-deployment.yaml index ce943f73c..7d42a6b64 100644 --- a/charts/orka/templates/harness-wrapper-deployment.yaml +++ b/charts/orka/templates/harness-wrapper-deployment.yaml @@ -1,9 +1,7 @@ -{{- include "orka.validateHarnessV1" . }} -{{- if eq .Values.controller.mode "harness-v1" }} apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "orka.harnessV1Name" . }} + name: {{ include "orka.harnessWrapperName" . }} labels: {{- include "orka.labels" . | nindent 4 }} app.kubernetes.io/component: agent-harness-wrapper @@ -16,5 +14,97 @@ spec: {{- include "orka.selectorLabels" . | nindent 6 }} app.kubernetes.io/component: agent-harness-wrapper template: - {{- include "orka.harnessV1PodTemplate" (dict "root" . "generation" (include "orka.harnessV1PodTemplateGeneration" .)) | nindent 4 }} -{{- end }} + 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-drain-hook.yaml b/charts/orka/templates/harness-wrapper-drain-hook.yaml deleted file mode 100644 index f43747028..000000000 --- a/charts/orka/templates/harness-wrapper-drain-hook.yaml +++ /dev/null @@ -1,490 +0,0 @@ -{{- $harnessV1 := eq .Values.controller.mode "harness-v1" -}} -{{- if $harnessV1 }} -{{- include "orka.validateHarnessV1" . }} -{{- $wrapperName := include "orka.harnessV1Name" . }} -{{- $desiredGeneration := include "orka.harnessV1PodTemplateGeneration" . }} -{{- $rolloverGeneration := $desiredGeneration }} -{{- $existingWrapper := lookup "apps/v1" "Deployment" .Release.Namespace $wrapperName }} -{{- $controllerName := include "orka.controllerName" . }} -{{- $existingController := lookup "apps/v1" "Deployment" .Release.Namespace $controllerName }} -{{- $existingControllerState := "" }} -{{- if $existingController }} -{{- $existingControllerState = include "orka.harnessV1ExistingControllerState" $existingController | trim }} -{{- end }} -{{- if and .Release.IsUpgrade (not $existingWrapper) (empty $existingControllerState) }} -{{- fail "cannot determine the previously deployed harness v1 state during upgrade; restore the release controller or wrapper Deployment before upgrading" }} -{{- end }} -{{- $needsRollover := false }} -{{- $existingRouteEnabled := false }} -{{- $existingImage := "" }} -{{- $existingPullPolicy := "" }} -{{- $existingAuthSecret := "" }} -{{- $existingAuthKey := "" }} -{{- $existingTLSSecret := "" }} -{{- $desiredAuthSecret := .Values.harnessV1.auth.existingSecret }} -{{- $desiredAuthKey := .Values.harnessV1.auth.tokenKey }} -{{- if $existingWrapper }} -{{- $existingRouteEnabled = true }} -{{- $existingGeneration := include "orka.harnessV1ExistingGeneration" $existingWrapper | trim }} -{{- $needsRollover = ne $existingGeneration $desiredGeneration }} -{{- $existingImage = include "orka.harnessV1ExistingImage" $existingWrapper }} -{{- $existingPullPolicy = include "orka.harnessV1ExistingImagePullPolicy" $existingWrapper }} -{{- $existingAuthSecret = include "orka.harnessV1ExistingAuthSecretName" $existingWrapper }} -{{- $existingAuthKey = include "orka.harnessV1ExistingAuthSecretKey" $existingWrapper }} -{{- $existingTLSSecret = include "orka.harnessV1ExistingTLSSecretName" $existingWrapper }} -{{- else if eq $existingControllerState "enabled" }} -{{- $existingRouteEnabled = true }} -{{- $existingAuthSecret = include "orka.harnessV1ExistingControllerAuthSecretName" $existingController }} -{{- $existingAuthKey = include "orka.harnessV1ExistingControllerAuthSecretKey" $existingController }} -{{- end }} -{{- if $existingRouteEnabled }} -{{- if ne $existingAuthSecret $desiredAuthSecret }} -{{- fail "harnessV1.auth.existingSecret cannot change while the previously deployed harness v1 route remains enabled; retire harness v1 before rotating wrapper auth" }} -{{- end }} -{{- if ne $existingAuthKey $desiredAuthKey }} -{{- fail "harnessV1.auth.tokenKey cannot change while the previously deployed harness v1 route remains enabled; retire harness v1 before rotating wrapper auth" }} -{{- end }} -{{- end }} -{{- if $needsRollover }} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "orka.harnessV1DrainName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-drain - annotations: - helm.sh/hook: pre-upgrade,pre-rollback - helm.sh/hook-weight: "-20" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper - policyTypes: [Ingress] - ingress: - - from: - - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 14 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-drain - ports: - - protocol: TCP - port: 8080 ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "orka.harnessV1DrainEgressName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-drain - annotations: - helm.sh/hook: pre-upgrade,pre-rollback - helm.sh/hook-weight: "-20" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-drain - policyTypes: [Egress] - 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: agent-harness-wrapper - ports: - - protocol: TCP - port: 8080 ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ include "orka.harnessV1DrainName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-drain - annotations: - helm.sh/hook: pre-upgrade,pre-rollback - helm.sh/hook-weight: "-10" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - backoffLimit: 0 - template: - metadata: - labels: - {{- include "orka.selectorLabels" . | nindent 8 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-drain - spec: - serviceAccountName: {{ include "orka.harnessV1Name" . }} - automountServiceAccountToken: false - restartPolicy: Never - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - containers: - - name: drain - image: {{ $existingImage | quote }} - imagePullPolicy: {{ $existingPullPolicy }} - command: ["/orka-agent-harness-wrapper"] - args: - - drain - - {{ printf "--endpoint=https://%s.%s.svc:8080" $wrapperName .Release.Namespace | quote }} - - --bearer-token-file=/var/run/orka/harness-wrapper-auth/token - - --ca-file=/var/run/orka/harness-wrapper-tls/ca.crt - - {{ printf "--timeout=%s" .Values.harnessV1.upgradeDrain.timeout | quote }} - - {{ printf "--poll-interval=%s" .Values.harnessV1.upgradeDrain.pollInterval | quote }} - - {{ printf "--next-generation=%s" $rolloverGeneration | quote }} - volumeMounts: - - name: auth - mountPath: /var/run/orka/harness-wrapper-auth - readOnly: true - - name: tls - mountPath: /var/run/orka/harness-wrapper-tls - readOnly: true - - name: tmp - mountPath: /tmp - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - capabilities: - drop: [ALL] - {{- with .Values.harnessV1.upgradeDrain.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: auth - secret: - secretName: {{ $existingAuthSecret | quote }} - defaultMode: 0440 - items: - - key: {{ $existingAuthKey | quote }} - path: token - - name: tls - secret: - secretName: {{ $existingTLSSecret | quote }} - defaultMode: 0440 - items: - - key: ca.crt - path: ca.crt - - name: tmp - emptyDir: {} -{{- end }} -{{- if $harnessV1 }} ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "orka.harnessV1AbortName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-abort - annotations: - helm.sh/hook: post-rollback - helm.sh/hook-weight: "-20" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper - policyTypes: [Ingress] - ingress: - - from: - - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 14 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-abort - ports: - - protocol: TCP - port: 8080 ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "orka.harnessV1AbortEgressName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-abort - annotations: - helm.sh/hook: post-rollback - helm.sh/hook-weight: "-20" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-abort - policyTypes: [Egress] - 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: agent-harness-wrapper - ports: - - protocol: TCP - port: 8080 ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ include "orka.harnessV1AbortName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-abort - annotations: - helm.sh/hook: post-rollback - helm.sh/hook-weight: "-10" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - backoffLimit: 0 - template: - metadata: - labels: - {{- include "orka.selectorLabels" . | nindent 8 }} - app.kubernetes.io/component: agent-harness-wrapper-rollover-abort - spec: - serviceAccountName: {{ include "orka.harnessV1Name" . }} - automountServiceAccountToken: false - restartPolicy: Never - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - containers: - - name: abort-rollover - image: {{ include "orka.imageRef" .Values.harnessV1.image | quote }} - imagePullPolicy: {{ .Values.harnessV1.image.pullPolicy }} - command: ["/orka-agent-harness-wrapper"] - args: - - abort-rollover - - {{ printf "--endpoint=https://%s.%s.svc:8080" $wrapperName .Release.Namespace | quote }} - - --bearer-token-file=/var/run/orka/harness-wrapper-auth/token - - --ca-file=/var/run/orka/harness-wrapper-tls/ca.crt - - {{ printf "--expected-generation=%s" $desiredGeneration | quote }} - - {{ printf "--timeout=%s" .Values.harnessV1.upgradeDrain.timeout | quote }} - volumeMounts: - - name: auth - mountPath: /var/run/orka/harness-wrapper-auth - readOnly: true - - name: tls - mountPath: /var/run/orka/harness-wrapper-tls - readOnly: true - - name: tmp - mountPath: /tmp - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - capabilities: - drop: [ALL] - {{- with .Values.harnessV1.upgradeDrain.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: auth - secret: - secretName: {{ .Values.harnessV1.auth.existingSecret | quote }} - defaultMode: 0440 - items: - - key: {{ .Values.harnessV1.auth.tokenKey | quote }} - path: token - - name: tls - secret: - secretName: {{ .Values.harnessV1.tls.existingSecret | quote }} - defaultMode: 0440 - items: - - key: ca.crt - path: ca.crt - - name: tmp - emptyDir: {} -{{- end }} -{{- if $harnessV1 }} ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "orka.harnessV1DeleteDrainName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-delete-drain - annotations: - helm.sh/hook: pre-delete - helm.sh/hook-weight: "-20" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper - policyTypes: [Ingress] - ingress: - - from: - - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 14 }} - app.kubernetes.io/component: agent-harness-wrapper-delete-drain - ports: - - protocol: TCP - port: 8080 ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "orka.harnessV1DeleteDrainEgressName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-delete-drain - annotations: - helm.sh/hook: pre-delete - helm.sh/hook-weight: "-20" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper-delete-drain - policyTypes: [Egress] - 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: agent-harness-wrapper - ports: - - protocol: TCP - port: 8080 ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ include "orka.harnessV1DeleteDrainName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper-delete-drain - annotations: - helm.sh/hook: pre-delete - helm.sh/hook-weight: "-10" - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - backoffLimit: 0 - template: - metadata: - labels: - {{- include "orka.selectorLabels" . | nindent 8 }} - app.kubernetes.io/component: agent-harness-wrapper-delete-drain - spec: - serviceAccountName: {{ include "orka.harnessV1Name" . }} - automountServiceAccountToken: false - restartPolicy: Never - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - seccompProfile: - type: RuntimeDefault - containers: - - name: drain - image: {{ include "orka.imageRef" .Values.harnessV1.image | quote }} - imagePullPolicy: {{ .Values.harnessV1.image.pullPolicy }} - command: ["/orka-agent-harness-wrapper"] - args: - - drain - - {{ printf "--endpoint=https://%s.%s.svc:8080" $wrapperName .Release.Namespace | quote }} - - --bearer-token-file=/var/run/orka/harness-wrapper-auth/token - - --ca-file=/var/run/orka/harness-wrapper-tls/ca.crt - - {{ printf "--timeout=%s" .Values.harnessV1.upgradeDrain.timeout | quote }} - - {{ printf "--poll-interval=%s" .Values.harnessV1.upgradeDrain.pollInterval | quote }} - - {{ printf "--next-generation=retired:%s" $desiredGeneration | quote }} - volumeMounts: - - name: auth - mountPath: /var/run/orka/harness-wrapper-auth - readOnly: true - - name: tls - mountPath: /var/run/orka/harness-wrapper-tls - readOnly: true - - name: tmp - mountPath: /tmp - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - capabilities: - drop: [ALL] - {{- with .Values.harnessV1.upgradeDrain.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: auth - secret: - secretName: {{ .Values.harnessV1.auth.existingSecret | quote }} - defaultMode: 0440 - items: - - key: {{ .Values.harnessV1.auth.tokenKey | quote }} - path: token - - name: tls - secret: - secretName: {{ .Values.harnessV1.tls.existingSecret | quote }} - defaultMode: 0440 - items: - - key: ca.crt - path: ca.crt - - name: tmp - emptyDir: {} -{{- end }} -{{- end }} diff --git a/charts/orka/templates/harness-wrapper-networkpolicy.yaml b/charts/orka/templates/harness-wrapper-networkpolicy.yaml deleted file mode 100644 index d07344804..000000000 --- a/charts/orka/templates/harness-wrapper-networkpolicy.yaml +++ /dev/null @@ -1,87 +0,0 @@ -{{- include "orka.validateHarnessV1" . }} -{{- if eq .Values.controller.mode "harness-v1" }} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "orka.harnessV1Name" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper -spec: - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: agent-harness-wrapper - policyTypes: [Ingress, Egress] - ingress: - - from: - - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 14 }} - app.kubernetes.io/component: controller - ports: - - protocol: TCP - port: 8080 - # V1 compatibility workloads may reach their release-local controller plus - # public HTTPS provider and read-only SCM endpoints. Private, local, - # link-local, multicast, and documentation networks remain denied, and no - # in-cluster publisher/proxy egress is granted. - egress: - - to: - - podSelector: - matchLabels: - {{- include "orka.selectorLabels" . | nindent 14 }} - app.kubernetes.io/component: controller - ports: - - protocol: TCP - port: {{ .Values.controller.apiPort }} - - 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/harness-wrapper-pvc.yaml b/charts/orka/templates/harness-wrapper-pvc.yaml deleted file mode 100644 index 70f413323..000000000 --- a/charts/orka/templates/harness-wrapper-pvc.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- include "orka.validateHarnessV1" . }} -{{- if eq .Values.controller.mode "harness-v1" }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "orka.harnessV1LedgerName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper - annotations: - helm.sh/resource-policy: keep -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.harnessV1.ledger.size }} - {{- with .Values.harnessV1.ledger.storageClass }} - storageClassName: {{ . | quote }} - {{- end }} -{{- end }} diff --git a/charts/orka/templates/harness-wrapper-secret.yaml b/charts/orka/templates/harness-wrapper-secret.yaml new file mode 100644 index 000000000..4a3a5c968 --- /dev/null +++ b/charts/orka/templates/harness-wrapper-secret.yaml @@ -0,0 +1,19 @@ +{{- 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/harness-wrapper-service.yaml b/charts/orka/templates/harness-wrapper-service.yaml index cbbdbd3d3..e6d5ddf10 100644 --- a/charts/orka/templates/harness-wrapper-service.yaml +++ b/charts/orka/templates/harness-wrapper-service.yaml @@ -1,9 +1,7 @@ -{{- include "orka.validateHarnessV1" . }} -{{- if eq .Values.controller.mode "harness-v1" }} apiVersion: v1 kind: Service metadata: - name: {{ include "orka.harnessV1Name" . }} + name: {{ include "orka.harnessWrapperName" . }} labels: {{- include "orka.labels" . | nindent 4 }} app.kubernetes.io/component: agent-harness-wrapper @@ -12,8 +10,7 @@ spec: {{- include "orka.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: agent-harness-wrapper ports: - - name: https + - name: http port: 8080 - targetPort: https + targetPort: http protocol: TCP -{{- end }} diff --git a/charts/orka/templates/harness-wrapper-serviceaccount.yaml b/charts/orka/templates/harness-wrapper-serviceaccount.yaml deleted file mode 100644 index 24a7b8014..000000000 --- a/charts/orka/templates/harness-wrapper-serviceaccount.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- include "orka.validateHarnessV1" . }} -{{- if eq .Values.controller.mode "harness-v1" }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "orka.harnessV1Name" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: agent-harness-wrapper -automountServiceAccountToken: false -{{- end }} diff --git a/charts/orka/templates/provider-proxy-deployment.yaml b/charts/orka/templates/provider-proxy-deployment.yaml deleted file mode 100644 index cba838b6a..000000000 --- a/charts/orka/templates/provider-proxy-deployment.yaml +++ /dev/null @@ -1,82 +0,0 @@ -{{- include "orka.validateProviderProxyConfig" . }} -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index 3e7d953f9..000000000 --- a/charts/orka/templates/provider-proxy-networkpolicy.yaml +++ /dev/null @@ -1,36 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index 4c49d9ce9..000000000 --- a/charts/orka/templates/provider-proxy-secret.yaml +++ /dev/null @@ -1,31 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index ac79d3248..000000000 --- a/charts/orka/templates/provider-proxy-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index c1785784f..000000000 --- a/charts/orka/templates/provider-proxy-serviceaccount.yaml +++ /dev/null @@ -1,10 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index e28838faa..000000000 --- a/charts/orka/templates/publisher-deployment.yaml +++ /dev/null @@ -1,113 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .Values.publisher.enabled (not .Values.scmEgressProxy.enabled) }} -{{- fail "publisher.enabled requires scmEgressProxy.enabled so public HTTPS egress remains proxy-only" }} -{{- end }} -{{- if and (eq .Values.controller.mode "harness-v2") .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: {{ printf "%d" (int64 .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 deleted file mode 100644 index 7833f4c23..000000000 --- a/charts/orka/templates/publisher-networkpolicy.yaml +++ /dev/null @@ -1,40 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index 4f503ec33..000000000 --- a/charts/orka/templates/publisher-pvc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index 1b36da859..000000000 --- a/charts/orka/templates/publisher-secret.yaml +++ /dev/null @@ -1,23 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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/charts/orka/templates/publisher-service.yaml b/charts/orka/templates/publisher-service.yaml deleted file mode 100644 index 9b561dbfc..000000000 --- a/charts/orka/templates/publisher-service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .Values.publisher.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: {{ include "orka.publisherName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} - app.kubernetes.io/component: workspace-publisher -spec: - selector: - {{- include "orka.selectorLabels" . | nindent 4 }} - app.kubernetes.io/component: workspace-publisher - ports: - - name: http - port: 8080 - targetPort: http -{{- end }} diff --git a/charts/orka/templates/rbac.yaml b/charts/orka/templates/rbac.yaml index 19f5c045a..ce98dd14e 100644 --- a/charts/orka/templates/rbac.yaml +++ b/charts/orka/templates/rbac.yaml @@ -1,191 +1,132 @@ {{- if .Values.rbac.create -}} -{{- $harnessV2 := eq .Values.controller.mode "harness-v2" -}} -# Controller tenant Role. The release namespace is also the immutable watch namespace. +# Controller ClusterRole apiVersion: rbac.authorization.k8s.io/v1 -kind: Role +kind: ClusterRole metadata: - name: {{ include "orka.controllerName" . }} - namespace: {{ .Release.Namespace }} + name: {{ include "orka.fullname" . }}-controller labels: {{- include "orka.labels" . | nindent 4 }} rules: + # Custom Resource permissions - apiGroups: ["core.orka.ai"] - resources: ["tasks", "tools", "agents", "agentruntimes", "runtimepools", "promptattempts", "runtimesessioncontrols", "publications", "controllerepochs", "externaleffects", "providers", "skills", "repositorymonitors", "repositoryscans", "substrateactorpools", "outboundaccesspolicies"] + resources: ["tasks", "tools", "agents", "agentruntimes", "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", "runtimepools/status", "promptattempts/status", "runtimesessioncontrols/status", "publications/status", "controllerepochs/status", "externaleffects/status", "providers/status", "skills/status", "repositorymonitors/status", "repositoryscans/status", "substrateactorpools/status", "outboundaccesspolicies/status"] + resources: ["tasks/status", "tools/status", "agents/status", "agentruntimes/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", "runtimepools/finalizers", "promptattempts/finalizers", "runtimesessioncontrols/finalizers", "publications/finalizers", "controllerepochs/finalizers", "externaleffects/finalizers", "providers/finalizers", "skills/finalizers", "repositorymonitors/finalizers", "repositoryscans/finalizers", "substrateactorpools/finalizers", "outboundaccesspolicies/finalizers"] + resources: ["tasks/finalizers", "tools/finalizers", "agents/finalizers", "agentruntimes/finalizers", "providers/finalizers", "skills/finalizers", "repositorymonitors/finalizers", "repositoryscans/finalizers", "substrateactorpools/finalizers"] verbs: ["update"] - {{- if $harnessV2 }} - apiGroups: ["gateway.orka.ai"] - resources: ["gateways", "gatewaybindings"] + resources: ["gatewayclasses", "gateways", "gatewaybindings"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["gateway.orka.ai"] - resources: ["gateways/status", "gatewaybindings/status"] + resources: ["gatewayclasses/status", "gateways/status", "gatewaybindings/status"] verbs: ["get", "update", "patch"] - apiGroups: ["gateway.orka.ai"] - resources: ["gateways/finalizers", "gatewaybindings/finalizers"] - verbs: ["update"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaces", "executionworkspaceclasses", "executionworkspacepools"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaces/status", "executionworkspaceclasses/status", "executionworkspacepools/status"] - verbs: ["get", "update", "patch"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaces/finalizers", "executionworkspaceclasses/finalizers"] + resources: ["gatewayclasses/finalizers", "gateways/finalizers", "gatewaybindings/finalizers"] verbs: ["update"] - - apiGroups: ["extensions.agents.x-k8s.io"] - resources: ["sandboxclaims"] - verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] - - apiGroups: ["extensions.agents.x-k8s.io"] - resources: ["sandboxtemplates", "sandboxwarmpools"] - verbs: ["get", "list", "watch"] - - apiGroups: ["agents.x-k8s.io"] - resources: ["sandboxes"] - verbs: ["get", "list", "watch"] - - apiGroups: ["ate.dev"] - resources: ["actortemplates"] - verbs: ["get", "list", "watch"] - {{- end }} + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + resourceNames: ["tasks.core.orka.ai", "gatewayclasses.gateway.orka.ai", "gateways.gateway.orka.ai", "gatewaybindings.gateway.orka.ai"] + verbs: ["get"] + + # Job permissions - apiGroups: ["batch"] resources: ["jobs"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["batch"] resources: ["cronjobs"] verbs: ["get", "list", "watch"] + + # Core resource permissions - apiGroups: [""] - resources: ["configmaps", "secrets", "pods", "services"] + resources: ["configmaps"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch", "create", "update", "delete"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["pods/log"] verbs: ["get"] - apiGroups: [""] - resources: ["pods/status", "endpoints", "persistentvolumeclaims", "replicationcontrollers"] + resources: ["pods/status"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["serviceaccounts"] verbs: ["get", "list", "watch", "create", "update"] - apiGroups: [""] - resources: ["serviceaccounts/token", "pods/portforward"] - verbs: ["create"] + resources: ["namespaces"] + verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch", "create", "patch"] + - apiGroups: [""] + resources: ["nodes", "services", "endpoints"] + verbs: ["get", "list", "watch"] - apiGroups: ["discovery.k8s.io"] resources: ["endpointslices"] verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["persistentvolumes", "persistentvolumeclaims", "replicationcontrollers"] + verbs: ["get", "list", "watch"] + + # Workload resource permissions (read-only, for chat K8s tools) - apiGroups: ["apps"] - resources: ["deployments", "replicasets"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: ["apps"] - resources: ["statefulsets", "daemonsets"] + resources: ["deployments", "replicasets", "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", "update", "patch", "delete"] + verbs: ["get", "list", "watch", "create", "delete"] + # Agent sandbox workspace backend: create/reattach/delete claims and connect to sandbox pods + - apiGroups: ["extensions.agents.x-k8s.io"] + resources: ["sandboxclaims"] + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] + - apiGroups: ["extensions.agents.x-k8s.io"] + resources: ["sandboxtemplates", "sandboxwarmpools"] + verbs: ["get", "list", "watch"] + - apiGroups: ["agents.x-k8s.io"] + resources: ["sandboxes"] + verbs: ["get", "list", "watch"] + # Substrate workspace backend: validate approved ActorTemplates before job creation + - apiGroups: ["ate.dev"] + resources: ["actortemplates"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods/portforward"] + verbs: ["create"] - apiGroups: ["autoscaling"] resources: ["horizontalpodautoscalers"] verbs: ["get", "list", "watch"] - apiGroups: ["policy"] resources: ["poddisruptionbudgets"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + verbs: ["get", "list", "watch"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "watch"] - apiGroups: ["events.k8s.io"] resources: ["events"] verbs: ["get", "list"] - apiGroups: ["metrics.k8s.io"] - resources: ["pods"] + resources: ["pods", "nodes"] verbs: ["get", "list"] + + # RBAC permissions (for chat K8s tools and managed worker bindings) + - apiGroups: ["rbac.authorization.k8s.io"] + 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"] verbs: ["get", "list", "watch", "create", "update", "delete"] - - apiGroups: ["coordination.k8s.io"] - resources: ["leases"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "orka.controllerName" . }} - namespace: {{ .Release.Namespace }} - labels: - {{- include "orka.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ include "orka.controllerName" . }} -subjects: - - kind: ServiceAccount - name: {{ include "orka.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} -{{- if $harnessV2 }} ---- -# Runtime child resources are confined to the v2 installation's runtime namespace. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ include "orka.controllerName" . }}-runtime - namespace: {{ .Values.controller.acpRuntime.namespace }} - labels: - {{- include "orka.labels" . | nindent 4 }} -rules: - - apiGroups: ["apps"] - resources: ["deployments", "replicasets"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: [""] - resources: ["pods", "services", "secrets"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: ["networking.k8s.io"] - resources: ["networkpolicies"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: ["policy"] - resources: ["poddisruptionbudgets"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "orka.controllerName" . }}-runtime - namespace: {{ .Values.controller.acpRuntime.namespace }} - labels: - {{- include "orka.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ include "orka.controllerName" . }}-runtime -subjects: - - kind: ServiceAccount - name: {{ include "orka.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} -{{- end }} ---- -# Minimal cluster-scoped controller authority. Namespaced workload access is never bound here. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "orka.controllerClusterRoleName" . }} - labels: - {{- include "orka.labels" . | nindent 4 }} -rules: - - apiGroups: [""] - resources: ["namespaces"] - resourceNames: - - {{ .Release.Namespace }} - {{- if $harnessV2 }} - - {{ .Values.controller.acpRuntime.namespace }} - {{- end }} - verbs: ["get"] - - apiGroups: ["authentication.k8s.io"] - resources: ["tokenreviews"] - verbs: ["create"] - - apiGroups: ["authorization.k8s.io"] - resources: ["subjectaccessreviews"] - verbs: ["create"] - apiGroups: ["rbac.authorization.k8s.io"] resources: ["clusterroles"] resourceNames: @@ -193,62 +134,42 @@ rules: - {{ include "orka.vendorWorkerClusterRoleName" . }} - {{ include "orka.containerWorkerClusterRoleName" . }} verbs: ["bind"] - {{- if $harnessV2 }} - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - resourceNames: ["tasks.core.orka.ai", "gatewayclasses.gateway.orka.ai", "gateways.gateway.orka.ai", "gatewaybindings.gateway.orka.ai"] - verbs: ["get"] - - apiGroups: ["core.orka.ai"] - resources: ["branchclaims"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: ["core.orka.ai"] - resources: ["branchclaims/status"] - verbs: ["get", "update", "patch"] - - apiGroups: ["core.orka.ai"] - resources: ["branchclaims/finalizers"] - verbs: ["update"] - - apiGroups: ["gateway.orka.ai"] - resources: ["gatewayclasses"] + + # TokenReview authenticates caller tokens; SubjectAccessReview authorizes Kubernetes callers + - apiGroups: ["authentication.k8s.io"] + resources: ["tokenreviews"] + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: ["subjectaccessreviews"] + verbs: ["create"] + + # Leader election + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: ["gateway.orka.ai"] - resources: ["gatewayclasses/status"] - verbs: ["get", "update", "patch"] - - apiGroups: ["gateway.orka.ai"] - resources: ["gatewayclasses/finalizers"] - verbs: ["update"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaceproviders"] - verbs: ["get", "list", "watch", "update", "patch"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaceproviders/status"] - verbs: ["get", "update", "patch"] - - apiGroups: ["workspace.orka.ai"] - resources: ["executionworkspaceproviders/finalizers"] - verbs: ["update"] - {{- end }} --- +# Controller ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "orka.controllerClusterRoleName" . }} + name: {{ include "orka.fullname" . }}-controller labels: {{- include "orka.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ include "orka.controllerClusterRoleName" . }} + name: {{ include "orka.fullname" . }}-controller subjects: - kind: ServiceAccount name: {{ include "orka.serviceAccountName" . }} namespace: {{ .Release.Namespace }} --- -# Client Role (for API access in this installation's namespace) +# Client ClusterRole (for API access) {{- if .Values.client.create }} apiVersion: rbac.authorization.k8s.io/v1 -kind: Role +kind: ClusterRole metadata: name: {{ include "orka.fullname" . }}-client - namespace: {{ include "orka.clientNamespace" . }} labels: {{- include "orka.labels" . | nindent 4 }} rules: @@ -283,12 +204,10 @@ rules: resources: ["skills"] verbs: ["get", "list", "watch"] - {{- if $harnessV2 }} # Generic gateway resource permissions (read-only; durable ledgers use the Orka REST API). - apiGroups: ["gateway.orka.ai"] resources: ["gateways", "gatewaybindings"] verbs: ["get", "list", "watch"] - {{- end }} # ConfigMap permissions (for results and sessions) - apiGroups: [""] @@ -296,21 +215,28 @@ rules: verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.controller.enforceNamespaceIsolation }} kind: RoleBinding metadata: name: {{ include "orka.fullname" . }}-client namespace: {{ include "orka.clientNamespace" . }} labels: {{- include "orka.labels" . | nindent 4 }} +{{- else }} +kind: ClusterRoleBinding +metadata: + name: {{ include "orka.fullname" . }}-client + labels: + {{- include "orka.labels" . | nindent 4 }} +{{- end }} roleRef: apiGroup: rbac.authorization.k8s.io - kind: Role + kind: ClusterRole name: {{ include "orka.fullname" . }}-client subjects: - kind: ServiceAccount name: {{ .Values.client.name }} namespace: {{ include "orka.clientNamespace" . }} -{{- if $harnessV2 }} --- # GatewayClass is cluster-scoped, so isolated clients need a separate ClusterRoleBinding for read access. apiVersion: rbac.authorization.k8s.io/v1 @@ -339,7 +265,6 @@ subjects: name: {{ .Values.client.name }} namespace: {{ include "orka.clientNamespace" . }} {{- end }} -{{- end }} --- # AI Worker Role (trusted worker with Kubernetes tool and code_exec permissions) apiVersion: rbac.authorization.k8s.io/v1 @@ -424,12 +349,20 @@ rules: verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.controller.enforceNamespaceIsolation }} kind: RoleBinding metadata: - name: {{ include "orka.aiWorkerRoleBindingName" . }} + name: {{ include "orka.aiWorkerClusterRoleBindingName" . }} namespace: {{ .Release.Namespace }} labels: {{- include "orka.labels" . | nindent 4 }} +{{- else }} +kind: ClusterRoleBinding +metadata: + name: {{ include "orka.aiWorkerClusterRoleBindingName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +{{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole @@ -510,12 +443,20 @@ rules: verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.controller.enforceNamespaceIsolation }} kind: RoleBinding metadata: - name: {{ include "orka.vendorWorkerRoleBindingName" . }} + name: {{ include "orka.vendorWorkerClusterRoleBindingName" . }} namespace: {{ .Release.Namespace }} labels: {{- include "orka.labels" . | nindent 4 }} +{{- else }} +kind: ClusterRoleBinding +metadata: + name: {{ include "orka.vendorWorkerClusterRoleBindingName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +{{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole @@ -542,12 +483,20 @@ rules: verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.controller.enforceNamespaceIsolation }} kind: RoleBinding metadata: - name: {{ include "orka.containerWorkerRoleBindingName" . }} + name: {{ include "orka.containerWorkerClusterRoleBindingName" . }} namespace: {{ .Release.Namespace }} labels: {{- include "orka.labels" . | nindent 4 }} +{{- else }} +kind: ClusterRoleBinding +metadata: + name: {{ include "orka.containerWorkerClusterRoleBindingName" . }} + labels: + {{- include "orka.labels" . | nindent 4 }} +{{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole diff --git a/charts/orka/templates/runtime-namespace.yaml b/charts/orka/templates/runtime-namespace.yaml deleted file mode 100644 index 94295f497..000000000 --- a/charts/orka/templates/runtime-namespace.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if eq .Values.controller.mode "harness-v2" }} -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 deleted file mode 100644 index 434969dc6..000000000 --- a/charts/orka/templates/scm-egress-proxy-deployment.yaml +++ /dev/null @@ -1,86 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 - enableServiceLinks: 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=%d" (int64 .Values.scmEgressProxy.maxRequestHeaderBytes) | quote }} - - {{ printf "--max-response-header-bytes=%d" (int64 .Values.scmEgressProxy.maxResponseHeaderBytes) | quote }} - - {{ printf "--max-request-bytes=%d" (int64 .Values.scmEgressProxy.maxRequestBytes) | quote }} - - {{ printf "--max-response-bytes=%d" (int64 .Values.scmEgressProxy.maxResponseBytes) | quote }} - - {{ printf "--max-tunnel-bytes=%d" (int64 .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 deleted file mode 100644 index 0687aa907..000000000 --- a/charts/orka/templates/scm-egress-proxy-networkpolicy.yaml +++ /dev/null @@ -1,63 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index 609dd6cf9..000000000 --- a/charts/orka/templates/scm-egress-proxy-secret.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index f57291c70..000000000 --- a/charts/orka/templates/scm-egress-proxy-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 deleted file mode 100644 index 11fc06334..000000000 --- a/charts/orka/templates/scm-egress-proxy-serviceaccount.yaml +++ /dev/null @@ -1,10 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 0bea13d7d..e7c65e6b9 100644 --- a/charts/orka/templates/serviceaccount.yaml +++ b/charts/orka/templates/serviceaccount.yaml @@ -11,21 +11,6 @@ metadata: {{- end }} {{- end }} --- -{{- if and (eq .Values.controller.mode "harness-v2") .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 @@ -63,3 +48,11 @@ 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 9463d9224..b13ba6e81 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.storeName" . }} + name: {{ include "orka.fullname" . }}-store 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 deleted file mode 100644 index b5549c1e9..000000000 --- a/charts/orka/templates/vekil-ingress-networkpolicy.yaml +++ /dev/null @@ -1,24 +0,0 @@ -{{- if and (eq .Values.controller.mode "harness-v2") .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 f752de399..355c8abcf 100644 --- a/charts/orka/values.yaml +++ b/charts/orka/values.yaml @@ -2,10 +2,6 @@ # Controller configuration controller: - # Required immutable execution contract for this installation. Install v1 - # and v2 as separate releases; one release never serves both contracts. - mode: "" - # Number of controller replicas replicas: 1 @@ -13,7 +9,6 @@ controller: image: repository: ghcr.io/orka-agents/orka tag: "0.1.1" - digest: "" pullPolicy: IfNotPresent # Resource limits @@ -25,8 +20,7 @@ controller: cpu: 100m memory: 256Mi - # Required exclusive tenant namespace. Label it with - # orka.ai/controller-mode matching controller.mode before startup. + # Watch namespace (empty for cluster-scoped) watchNamespace: "" # API server port @@ -38,60 +32,18 @@ controller: # Health probe port healthPort: 8082 - # Leader election is mandatory for the singleton SQLite controller. + # Enable leader election for HA leaderElect: true # Log level (debug, info, warn, error) logLevel: info + # Enforce namespace isolation (restrict users to their SA namespace) + enforceNamespaceIsolation: true + # 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 - - # Encryption key for immutable agent execution snapshots. When either agent - # execution protocol is enabled, both fields must reference an existing - # Secret. The selected value must contain exactly 32 raw bytes or its base64 - # encoding. The Secret name, item key, and selected key material must remain - # unchanged for the lifetime of the release so retained snapshots stay - # decryptable. - agentExecutionSnapshot: - existingSecret: "" - key: "" - # A snapshot must remain unreferenced for this full period before GC. - retention: 720h - # Reference-aware GC scan cadence. - retentionInterval: 1h - - # Managed ACP core RuntimePools for built-in agent Tasks. - acpRuntime: - # Used only when controller.mode is harness-v2; there is no v1 fallback. - 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: "" - opencodeImage: "" - # 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 @@ -221,206 +173,6 @@ controller: childTokenTTL: "" toolTokenTTL: "" -# Fail-closed admission served by this release's internal webhook Service. The -# certificate must authenticate -orka-webhook..svc. -webhooks: - tls: - # Required. The chart never generates webhook certificates. - existingSecret: "" - certKey: tls.crt - privateKeyKey: tls.key - # Base64-encoded PEM CA bundle. Leave empty when an injector annotation is - # configured below. - caBundle: "" - # For example: cert-manager.io/inject-ca-from-secret: orka-v2/orka-webhook-tls - caInjectionAnnotations: {} - timeoutSeconds: 10 - -# 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 - -# orka.harness.v1 data plane configuration. It is rendered only when -# controller.mode is harness-v1 and is never an ACP fallback. -harnessV1: - dispatch: - interval: 1s - workers: 1 - upgradeDrain: - # Before an upgrade, rollback, or deletion mutates an existing wrapper, a - # Helm hook durably closes admission and proves that no unsettled turns - # remain. Keep this below Helm's default five-minute operation timeout. - timeout: 4m - pollInterval: 2s - resources: - requests: - cpu: 10m - memory: 32Mi - ephemeral-storage: 16Mi - limits: - cpu: 100m - memory: 64Mi - ephemeral-storage: 64Mi - image: - repository: ghcr.io/orka-agents/orka/agent-harness-wrapper - tag: "0.1.1" - # Required when controller.mode is harness-v1. Tags are never accepted for - # a running wrapper. - digest: "" - pullPolicy: IfNotPresent - auth: - # Use a dedicated operator-created Secret. Bearer credentials are never - # accepted through Helm values or emitted into rendered release manifests. - # The Secret source and key are immutable while a harness-v1 wrapper - # Deployment exists because controller attempts pin its resourceVersion. - existingSecret: "" - tokenKey: token - tls: - # Use a different operator-created Secret containing tls.crt, tls.key, and - # ca.crt. Its serving certificate must authenticate the release-scoped - # Service DNS name: -orka-agent-harness-wrapper..svc. - existingSecret: "" - # Bump this non-secret revision whenever certificate data changes without - # changing existingSecret. The chart drains the live wrapper before it - # restarts both the wrapper and controller onto the new TLS generation. - rolloutNonce: "" - ledger: - # The admission ledger is always PVC-backed when harness v1 is enabled. - size: 1Gi - storageClass: "" - # Controller-acknowledged terminal records remain replay-safe for this - # audit/backup window before bounded wrapper GC may reclaim them. - retention: 720h - # Codex defaults to workspace-write. Broader modes require explicit policy. - codexSandboxMode: workspace-write - resources: - requests: - cpu: 100m - memory: 256Mi - ephemeral-storage: 512Mi - limits: - cpu: "2" - memory: 2Gi - ephemeral-storage: 2Gi - # Worker configuration workers: ai: @@ -434,6 +186,21 @@ 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: @@ -514,7 +281,7 @@ annotations: {} store: path: /data/orka.db persistence: - enabled: true # ACP control records, gateway history, and Session state require durable RWO storage + enabled: true # durable gateway inbox/outbox and Session history require a PVC size: 1Gi storageClass: "" # use cluster default accessMode: ReadWriteOnce @@ -525,7 +292,6 @@ client: create: true # Name of the client service account name: orka-client - # Namespace for the client service account. Empty defaults to - # controller.watchNamespace. A different namespace is not supported by an - # isolated installation. + # Namespace for the client service account. Empty defaults to controller.watchNamespace + # when namespace isolation is enforced and watchNamespace is set, otherwise the release namespace. namespace: "" diff --git a/cmd/build/helmify/static/templates/rbac.yaml b/cmd/build/helmify/static/templates/rbac.yaml index 19f5c045a..c4ffe2fa8 100644 --- a/cmd/build/helmify/static/templates/rbac.yaml +++ b/cmd/build/helmify/static/templates/rbac.yaml @@ -31,6 +31,15 @@ rules: - apiGroups: ["workspace.orka.ai"] resources: ["executionworkspaces", "executionworkspaceclasses", "executionworkspacepools"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # The workspace admission policies gate spec.coreAdmission writes on the + # "admit" verb and class-backed Task/Tool creation on the "use" verb, so the + # controller needs both or fail-closed admission rejects its own writes. + - apiGroups: ["workspace.orka.ai"] + resources: ["executionworkspaces"] + verbs: ["admit"] + - apiGroups: ["workspace.orka.ai"] + resources: ["executionworkspaceclasses"] + verbs: ["use"] - apiGroups: ["workspace.orka.ai"] resources: ["executionworkspaces/status", "executionworkspaceclasses/status", "executionworkspacepools/status"] verbs: ["get", "update", "patch"] diff --git a/cmd/build/helmify/static/templates/workspace-class-use-policy.yaml b/cmd/build/helmify/static/templates/workspace-class-use-policy.yaml new file mode 100644 index 000000000..1a8c61ec7 --- /dev/null +++ b/cmd/build/helmify/static/templates/workspace-class-use-policy.yaml @@ -0,0 +1,72 @@ +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-core-admission-policy.yaml b/cmd/build/helmify/static/templates/workspace-core-admission-policy.yaml new file mode 100644 index 000000000..47dd68f2a --- /dev/null +++ b/cmd/build/helmify/static/templates/workspace-core-admission-policy.yaml @@ -0,0 +1,44 @@ +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/orka-provider-auth-proxy/proxy.go b/cmd/orka-provider-auth-proxy/proxy.go index 868f02b9c..7c9e95782 100644 --- a/cmd/orka-provider-auth-proxy/proxy.go +++ b/cmd/orka-provider-auth-proxy/proxy.go @@ -16,22 +16,20 @@ import ( "net/url" "strings" "time" + + "github.com/orka-agents/orka/internal/providerproxy" ) 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" + 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" ) var errRequestBodyTooLarge = errors.New("provider request body exceeds limit") @@ -107,7 +105,7 @@ func normalizeProxyConfig(cfg proxyConfig) (proxyConfig, *url.URL, error) { if parsed.Path == "" { parsed.Path = "/" } - if hasUnsafePathSegment(parsed.Path) { + if providerproxy.HasUnsafePathSegment(parsed.Path) { return proxyConfig{}, nil, fmt.Errorf("provider upstream base URL is invalid") } if cfg.MaxRequestBytes <= 0 { @@ -145,7 +143,7 @@ func (p *providerAuthProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if r.URL.Path == readinessPath { if !p.tokens.isReady() { - writeProxyError(w, http.StatusServiceUnavailable, "provider proxy authentication is unavailable") + providerproxy.WriteError(w, http.StatusServiceUnavailable, "provider proxy authentication is unavailable") return } serveHealth(w, r) @@ -153,67 +151,68 @@ func (p *providerAuthProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } 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") + providerproxy.WriteError(w, http.StatusUnauthorized, "provider proxy authentication required") return } - if !tryAcquire(p.requestSlots) { - writeProxyError(w, http.StatusTooManyRequests, "provider proxy request capacity is exhausted") + if !providerproxy.TryAcquireSlot(p.requestSlots) { + providerproxy.WriteError(w, http.StatusTooManyRequests, "provider proxy request capacity is exhausted") return } - defer release(p.requestSlots) + defer providerproxy.ReleaseSlot(p.requestSlots) if r.Method == http.MethodConnect || r.Method == http.MethodTrace { - writeProxyError(w, http.StatusMethodNotAllowed, "provider request method is not allowed") + providerproxy.WriteError(w, http.StatusMethodNotAllowed, "provider request method is not allowed") return } - if hasUnsafePathSegment(r.URL.Path) { - writeProxyError(w, http.StatusBadRequest, "provider request path is invalid") + if providerproxy.HasUnsafePathSegment(r.URL.Path) { + providerproxy.WriteError(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") + if providerproxy.HasDisallowedContentEncoding(r.Header) { + providerproxy.WriteError(w, http.StatusUnsupportedMediaType, "compressed provider requests are forbidden") return } if r.ContentLength > p.maxRequestBytes { - writeProxyError(w, http.StatusRequestEntityTooLarge, "provider request body exceeds limit") + providerproxy.WriteError(w, http.StatusRequestEntityTooLarge, "provider request body exceeds limit") return } - target := providerTarget(p.upstreamBase, r.URL.Path, r.URL.RawQuery) + target := providerproxy.Target(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") + providerproxy.WriteError(w, http.StatusBadGateway, "provider request could not be prepared") return } upstreamRequest.ContentLength = r.ContentLength - copyProviderRequestHeaders(upstreamRequest.Header, r.Header) + providerproxy.CopyRequestHeaders(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") + providerproxy.WriteError(w, http.StatusRequestEntityTooLarge, "provider request body exceeds limit") return } - writeProxyError(w, http.StatusBadGateway, "provider upstream request failed") + providerproxy.WriteError(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") + providerproxy.WriteError(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") + if providerproxy.HasDisallowedContentEncoding(response.Header) { + providerproxy.WriteError(w, http.StatusBadGateway, "compressed provider responses are forbidden") return } if response.ContentLength > p.maxResponseBytes { - writeProxyError(w, http.StatusBadGateway, "provider upstream response exceeds limit") + providerproxy.WriteError(w, http.StatusBadGateway, "provider upstream response exceeds limit") return } - copyProviderResponseHeaders(w.Header(), response.Header) + providerproxy.CopyResponseHeaders(w.Header(), response.Header) w.WriteHeader(response.StatusCode) - if err := streamBoundedResponse(w, response.Body, p.maxResponseBytes); err != nil { + // The nil flusher keeps this proxy's original buffered write behavior. + if err := providerproxy.StreamBoundedResponse(w, response.Body, p.maxResponseBytes, nil); err != nil { panic(http.ErrAbortHandler) } } @@ -225,7 +224,7 @@ func (p *providerAuthProxy) authorized(values []string) bool { 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") + providerproxy.WriteError(w, http.StatusMethodNotAllowed, "health probe method is not allowed") return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") @@ -256,156 +255,6 @@ func (r *boundedReadCloser) Read(buffer []byte) (int, error) { 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, diff --git a/cmd/orka-provider-auth-proxy/proxy_test.go b/cmd/orka-provider-auth-proxy/proxy_test.go index 11b9d64f3..5d127b431 100644 --- a/cmd/orka-provider-auth-proxy/proxy_test.go +++ b/cmd/orka-provider-auth-proxy/proxy_test.go @@ -46,7 +46,7 @@ func TestProviderAuthProxyForwardsAuthorizedRequestWithoutSensitiveHeaders(t *te 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"} { + for _, name := range []string{authorizationHeader, "X-Api-Key", "Cookie", "Txn-Token", "X-Orka-Internal"} { if value := r.Header.Get(name); value != "" { t.Fatalf("upstream received sensitive header %s", name) } @@ -72,7 +72,7 @@ func TestProviderAuthProxyForwardsAuthorizedRequestWithoutSensitiveHeaders(t *te 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("X-Api-Key", "child-key") request.Header.Set("Cookie", "child=cookie") request.Header.Set("Txn-Token", "transaction") request.Header.Set("X-Orka-Internal", "internal") @@ -106,7 +106,7 @@ func TestProviderAuthProxyRejectsRedirectsAndCompressedResponses(t *testing.T) { w.WriteHeader(http.StatusTemporaryRedirect) }, "compressed": func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set(providerContentEncodingHeader, "gzip") + w.Header().Set("Content-Encoding", "gzip") _, _ = io.WriteString(w, "compressed") }, } { diff --git a/cmd/orka-scm-egress-proxy/proxy.go b/cmd/orka-scm-egress-proxy/proxy.go index fa63fe8a2..0b913963a 100644 --- a/cmd/orka-scm-egress-proxy/proxy.go +++ b/cmd/orka-scm-egress-proxy/proxy.go @@ -19,6 +19,7 @@ import ( "net/textproto" "net/url" "strings" + "sync/atomic" "time" ) @@ -171,18 +172,25 @@ func (p *scmEgressProxy) runTunnel(client, upstream net.Conn, buffered *bufio.Re if err := buffered.Flush(); err != nil { return } + activity := newTunnelActivity() clientBudget := p.config.MaxTunnelBytes if buffered.Reader.Buffered() > 0 { - count, err := copyBuffered(upstream, buffered.Reader, clientBudget, p.config.IdleTimeout) + count, err := copyBuffered(upstream, buffered.Reader, clientBudget, p.config.IdleTimeout, activity) if err != nil { return } clientBudget -= count } - p.copyTunnel(client, upstream, clientBudget) + p.copyTunnel(client, upstream, clientBudget, activity) } -func copyBuffered(destination net.Conn, reader *bufio.Reader, limit int64, idleTimeout time.Duration) (int64, error) { +func copyBuffered( + destination net.Conn, + reader *bufio.Reader, + limit int64, + idleTimeout time.Duration, + activity *tunnelActivity, +) (int64, error) { buffered := reader.Buffered() if int64(buffered) > limit { return 0, errTunnelLimit @@ -191,32 +199,51 @@ func copyBuffered(destination net.Conn, reader *bufio.Reader, limit int64, idleT if _, err := io.ReadFull(reader, data); err != nil { return 0, err } - deadline := &deadlineConn{Conn: destination, idleTimeout: idleTimeout} + deadline := &deadlineConn{Conn: destination, idleTimeout: idleTimeout, activity: activity} 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) { +func (p *scmEgressProxy) copyTunnel(client, upstream net.Conn, clientBudget int64, activity *tunnelActivity) { results := make(chan error, 2) go func() { - results <- boundedTunnelCopy( - &deadlineConn{Conn: upstream, idleTimeout: p.config.IdleTimeout}, - &deadlineConn{Conn: client, idleTimeout: p.config.IdleTimeout}, + err := boundedTunnelCopy( + &deadlineConn{Conn: upstream, idleTimeout: p.config.IdleTimeout, activity: activity}, + &deadlineConn{Conn: client, idleTimeout: p.config.IdleTimeout, activity: activity}, clientBudget, ) + if err == nil { + // The client finished sending cleanly; propagate the half-close so + // the upstream response direction can keep streaming. + closeWriteSide(upstream) + } + results <- err }() go func() { - results <- boundedTunnelCopy( - &deadlineConn{Conn: client, idleTimeout: p.config.IdleTimeout}, - &deadlineConn{Conn: upstream, idleTimeout: p.config.IdleTimeout}, + err := boundedTunnelCopy( + &deadlineConn{Conn: client, idleTimeout: p.config.IdleTimeout, activity: activity}, + &deadlineConn{Conn: upstream, idleTimeout: p.config.IdleTimeout, activity: activity}, p.config.MaxTunnelBytes, ) + if err == nil { + closeWriteSide(client) + } + results <- err }() timer := time.NewTimer(p.config.TunnelTimeout) defer timer.Stop() - select { - case <-results: - case <-timer.C: + // A clean EOF in one direction must not tear down an active transfer in + // the other, so wait for both directions unless one fails or the overall + // tunnel budget expires. + for range 2 { + select { + case err := <-results: + if err != nil { + return + } + case <-timer.C: + return + } } } @@ -228,23 +255,80 @@ func boundedTunnelCopy(destination io.Writer, source io.Reader, limit int64) err return err } +// tunnelActivity tracks the last byte movement across both tunnel directions +// so idle enforcement applies to the tunnel as a whole: a quiet request +// direction must not time out an active response direction. +type tunnelActivity struct { + lastNanos atomic.Int64 +} + +func newTunnelActivity() *tunnelActivity { + activity := &tunnelActivity{} + activity.touch() + return activity +} + +func (a *tunnelActivity) touch() { + a.lastNanos.Store(time.Now().UnixNano()) +} + +func (a *tunnelActivity) idleFor(idle time.Duration) bool { + return time.Since(time.Unix(0, a.lastNanos.Load())) >= idle +} + +type writeCloser interface { + CloseWrite() error +} + +func closeWriteSide(conn net.Conn) { + if half, ok := conn.(writeCloser); ok { + _ = half.CloseWrite() + } +} + type deadlineConn struct { net.Conn idleTimeout time.Duration + activity *tunnelActivity } func (c *deadlineConn) Read(value []byte) (int, error) { - if err := c.SetReadDeadline(time.Now().Add(c.idleTimeout)); err != nil { - return 0, err + for { + if err := c.SetReadDeadline(time.Now().Add(c.idleTimeout)); err != nil { + return 0, err + } + read, err := c.Conn.Read(value) + if read > 0 { + c.activity.touch() + } + if read == 0 && isTimeoutError(err) && !c.activity.idleFor(c.idleTimeout) { + continue + } + return read, 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 + written := 0 + for { + if err := c.SetWriteDeadline(time.Now().Add(c.idleTimeout)); err != nil { + return written, err + } + count, err := c.Conn.Write(value[written:]) + written += count + if count > 0 { + c.activity.touch() + } + if written < len(value) && isTimeoutError(err) && !c.activity.idleFor(c.idleTimeout) { + continue + } + return written, err } - return c.Conn.Write(value) +} + +func isTimeoutError(err error) bool { + var netErr net.Error + return errors.As(err, &netErr) && netErr.Timeout() } func (p *scmEgressProxy) handleForward(writer http.ResponseWriter, request *http.Request) { diff --git a/cmd/orka-scm-egress-proxy/proxy_test.go b/cmd/orka-scm-egress-proxy/proxy_test.go index 0f257704a..29aa6af1a 100644 --- a/cmd/orka-scm-egress-proxy/proxy_test.go +++ b/cmd/orka-scm-egress-proxy/proxy_test.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "bytes" "context" "crypto/tls" "encoding/base64" @@ -142,6 +143,152 @@ func TestSCMEgressProxyAllowsAuthenticatedGitHubCONNECT(t *testing.T) { } } +func TestSCMEgressProxyCONNECTSurvivesQuietRequestDirection(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen upstream: %v", err) + } + defer func() { _ = listener.Close() }() + const ( + chunkCount = 12 + chunkGap = 100 * time.Millisecond + ) + chunk := bytes.Repeat([]byte("x"), 1024) + go func() { + connection, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer func() { _ = connection.Close() }() + request := make([]byte, 4) + if _, readErr := io.ReadFull(connection, request); readErr != nil { + return + } + for range chunkCount { + if _, writeErr := connection.Write(chunk); writeErr != nil { + return + } + time.Sleep(chunkGap) + } + }() + publicAddress := netip.MustParseAddr("1.1.1.1") + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + // The response stream outlives the idle timeout, so the quiet request + // direction hits its read deadline repeatedly while the response + // direction is still moving bytes. + IdleTimeout: 500 * time.Millisecond, + Resolver: resolverFunc(func(context.Context, string, string) ([]netip.Addr, error) { + return []netip.Addr{publicAddress}, nil + }), + Dialer: localTestDialer(t, listener.Addr().String(), publicAddress), + }) + server := httptest.NewServer(proxy) + defer server.Close() + + connection, reader := openTestCONNECTTunnel(t, server.URL) + if err := connection.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil { + t.Fatalf("set read deadline: %v", err) + } + if _, err := connection.Write([]byte("ping")); err != nil { + t.Fatalf("write tunnel payload: %v", err) + } + payload := make([]byte, chunkCount*len(chunk)) + if _, err := io.ReadFull(reader, payload); err != nil { + t.Fatalf("read streamed payload: %v", err) + } +} + +func TestSCMEgressProxyCONNECTHalfCloseKeepsResponseStreaming(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen upstream: %v", err) + } + defer func() { _ = listener.Close() }() + payload := bytes.Repeat([]byte("y"), 64<<10) + go func() { + connection, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer func() { _ = connection.Close() }() + // The upstream only responds after the request direction is fully + // drained to EOF, which requires the proxy to propagate the client + // half-close instead of tearing the tunnel down. + if _, copyErr := io.Copy(io.Discard, connection); copyErr != nil { + return + } + _, _ = connection.Write(payload) + }() + publicAddress := netip.MustParseAddr("1.1.1.1") + proxy := newTestSCMProxy(t, proxyConfig{ + AllowedHosts: map[string]struct{}{"github.com": {}}, + IdleTimeout: 2 * time.Second, + Resolver: resolverFunc(func(context.Context, string, string) ([]netip.Addr, error) { + return []netip.Addr{publicAddress}, nil + }), + Dialer: localTestDialer(t, listener.Addr().String(), publicAddress), + }) + server := httptest.NewServer(proxy) + defer server.Close() + + connection, reader := openTestCONNECTTunnel(t, server.URL) + if err := connection.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil { + t.Fatalf("set read deadline: %v", err) + } + if _, err := connection.Write([]byte("ping")); err != nil { + t.Fatalf("write tunnel payload: %v", err) + } + tcpConnection, ok := connection.(*net.TCPConn) + if !ok { + t.Fatalf("connection type = %T, want *net.TCPConn", connection) + } + if err := tcpConnection.CloseWrite(); err != nil { + t.Fatalf("half-close tunnel: %v", err) + } + received := make([]byte, len(payload)) + if _, err := io.ReadFull(reader, received); err != nil { + t.Fatalf("read payload after half-close: %v", err) + } + if !bytes.Equal(received, payload) { + t.Fatal("payload mismatch after half-close") + } +} + +func openTestCONNECTTunnel(t *testing.T, proxyURL string) (net.Conn, *bufio.Reader) { + t.Helper() + connection, err := net.DialTimeout("tcp", strings.TrimPrefix(proxyURL, "http://"), time.Second) + if err != nil { + t.Fatalf("dial proxy: %v", err) + } + t.Cleanup(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 + } + } + return connection, reader +} + func TestSCMEgressProxyRejectsRedirect(t *testing.T) { upstream := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { writer.Header().Set("Location", "https://github.com/redirected") @@ -370,6 +517,13 @@ type remoteAddressConn struct { func (connection *remoteAddressConn) RemoteAddr() net.Addr { return connection.remote } +func (connection *remoteAddressConn) CloseWrite() error { + if half, ok := connection.Conn.(writeCloser); ok { + return half.CloseWrite() + } + return nil +} + func localTestDialer(t *testing.T, localAddress string, publicAddress netip.Addr) dialerFunc { t.Helper() return func(ctx context.Context, network, address string) (net.Conn, error) { diff --git a/deploy/orka.yaml b/deploy/orka.yaml index 84548d354..c955a68da 100644 --- a/deploy/orka.yaml +++ b/deploy/orka.yaml @@ -1,37 +1,6737 @@ apiVersion: v1 kind: Namespace -metadata: - labels: - app.kubernetes.io/component: acp-runtime - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - pod-security.kubernetes.io/audit: restricted - pod-security.kubernetes.io/enforce: baseline - pod-security.kubernetes.io/warn: restricted - name: orka-runtimes ---- -apiVersion: v1 -kind: Namespace metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka control-plane: controller-manager - orka.ai/controller-mode: harness-v2 pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/enforce: baseline 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 +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: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +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: {} +--- apiVersion: v1 -automountServiceAccountToken: true kind: ServiceAccount metadata: labels: - app.kubernetes.io/component: admission + app.kubernetes.io/component: agent-harness-wrapper app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-admission + name: orka-agent-harness-wrapper namespace: orka-system --- apiVersion: v1 @@ -56,102 +6756,11 @@ metadata: namespace: orka-system --- apiVersion: v1 -automountServiceAccountToken: false -kind: ServiceAccount -metadata: - 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: - 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: name: orka-vendor-worker namespace: orka-system --- -apiVersion: v1 -automountServiceAccountToken: false -kind: ServiceAccount -metadata: - 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 -kind: Role -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-runtime-controller-role - namespace: orka-runtimes -rules: -- apiGroups: - - apps - resources: - - deployments - - replicasets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - "" - resources: - - pods - - secrets - - services - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - networking.k8s.io - resources: - - networkpolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - policy - resources: - - poddisruptionbudgets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch ---- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -195,28 +6804,6 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole -metadata: - labels: - app.kubernetes.io/component: admission - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-admission -rules: -- apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create -- apiGroups: - - "" - resources: - - namespaces - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize @@ -358,7 +6945,6 @@ rules: - core.orka.ai resources: - tools - - outboundaccesspolicies verbs: - get - list @@ -573,37 +7159,20 @@ 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 + name: orka-gateway-admin-role rules: - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakepoolparameters + - gateways + - gatewaybindings verbs: - '*' - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakepoolparameters/status + - gateways/status + - gatewaybindings/status verbs: - get --- @@ -613,12 +7182,13 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-fake.workspace-fakepoolparameters-editor-role + name: orka-gateway-editor-role rules: - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakepoolparameters + - gateways + - gatewaybindings verbs: - create - delete @@ -628,9 +7198,10 @@ rules: - update - watch - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakepoolparameters/status + - gateways/status + - gatewaybindings/status verbs: - get --- @@ -640,22 +7211,19 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-fake.workspace-fakepoolparameters-viewer-role + name: orka-gateway-viewer-role rules: - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakepoolparameters + - gateways + - gatewaybindings + - gateways/status + - gatewaybindings/status verbs: - get - list - watch -- apiGroups: - - fake.workspace.orka.ai - resources: - - fakepoolparameters/status - verbs: - - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -663,18 +7231,18 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-fake.workspace-fakeproviderconfig-admin-role + name: orka-gatewayclass-admin-role rules: - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakeproviderconfigs + - gatewayclasses verbs: - '*' - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakeproviderconfigs/status + - gatewayclasses/status verbs: - get --- @@ -684,218 +7252,84 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-fake.workspace-fakeproviderconfig-editor-role + name: orka-gatewayclass-viewer-role rules: - apiGroups: - - fake.workspace.orka.ai + - gateway.orka.ai resources: - - fakeproviderconfigs + - gatewayclasses + - gatewayclasses/status 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 + name: orka-manager-role rules: - apiGroups: - - fake.workspace.orka.ai + - "" resources: - - fakeproviderconfigs + - configmaps verbs: + - create + - delete - get - list + - patch + - update - watch - apiGroups: - - fake.workspace.orka.ai + - "" resources: - - fakeproviderconfigs/status + - endpoints + - namespaces + - nodes + - persistentvolumeclaims + - persistentvolumes + - pods + - pods/status + - replicationcontrollers + - services 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-gateway-admin-role -rules: + - list + - watch - apiGroups: - - gateway.orka.ai + - "" resources: - - gateways - - gatewaybindings + - events verbs: - - '*' + - create + - get + - list + - patch + - watch - apiGroups: - - gateway.orka.ai + - "" resources: - - gateways/status - - gatewaybindings/status + - pods/log 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-gateway-editor-role -rules: - apiGroups: - - gateway.orka.ai + - "" resources: - - gateways - - gatewaybindings + - pods/portforward verbs: - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - gateway.orka.ai - resources: - - gateways/status - - gatewaybindings/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-gateway-viewer-role -rules: -- apiGroups: - - gateway.orka.ai - resources: - - gateways - - gatewaybindings - - gateways/status - - gatewaybindings/status - 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-gatewayclass-admin-role -rules: -- apiGroups: - - gateway.orka.ai - resources: - - gatewayclasses - verbs: - - '*' -- apiGroups: - - gateway.orka.ai - resources: - - gatewayclasses/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-gatewayclass-viewer-role -rules: -- apiGroups: - - gateway.orka.ai - resources: - - gatewayclasses - - gatewayclasses/status - verbs: - - get - - list - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: orka-manager-role -rules: - apiGroups: - "" resources: - - configmaps - - namespaces - - pods - secrets - - services verbs: - create - delete - get - list - - patch - update - watch -- apiGroups: - - "" - resources: - - endpoints - - nodes - - persistentvolumeclaims - - persistentvolumes - - pods/status - - replicationcontrollers - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - events - verbs: - - create - - get - - list - - patch - - watch -- apiGroups: - - "" - resources: - - pods/log - verbs: - - get -- apiGroups: - - "" - resources: - - pods/portforward - - serviceaccounts/token - verbs: - - create - apiGroups: - "" resources: @@ -929,23 +7363,12 @@ 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 @@ -955,12 +7378,6 @@ rules: - get - list - watch -- apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - apiGroups: - autoscaling resources: @@ -1006,16 +7423,9 @@ rules: resources: - agentruntimes - agents - - branchclaims - - controllerepochs - - externaleffects - - promptattempts - providers - - publications - repositorymonitors - repositoryscans - - runtimepools - - runtimesessioncontrols - skills - substrateactorpools - tasks @@ -1033,17 +7443,9 @@ 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 @@ -1055,17 +7457,9 @@ 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 @@ -1074,14 +7468,6 @@ rules: - get - patch - update -- apiGroups: - - core.orka.ai - resources: - - outboundaccesspolicies - verbs: - - get - - list - - watch - apiGroups: - discovery.k8s.io resources: @@ -1175,28 +7561,26 @@ 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: - - clusterroles + - clusterrolebindings + - rolebindings verbs: + - create + - delete - get - list + - update - watch - apiGroups: - rbac.authorization.k8s.io @@ -1211,14 +7595,11 @@ rules: - apiGroups: - rbac.authorization.k8s.io resources: - - rolebindings + - clusterroles - roles verbs: - - create - - delete - get - list - - update - watch - apiGroups: - storage.k8s.io @@ -1228,69 +7609,6 @@ rules: - get - list - watch -- apiGroups: - - workspace.orka.ai - resources: - - executionworkspaceclasses - verbs: - - create - - delete - - get - - list - - 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 @@ -1322,33 +7640,6 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-static-controller-cluster-role -rules: -- apiGroups: - - "" - resourceNames: - - orka-system - resources: - - namespaces - verbs: - - get -- apiGroups: - - rbac.authorization.k8s.io - resourceNames: - - orka-ai-worker-role - - orka-vendor-worker-role - - orka-container-worker-role - resources: - - clusterroles - verbs: - - bind ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize @@ -1492,125 +7783,20 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-v2-controller-cluster-role + name: orka-vendor-worker-role rules: - apiGroups: - "" - resourceNames: - - orka-runtimes resources: - - namespaces + - configmaps verbs: - get -- apiGroups: - - apiextensions.k8s.io - resourceNames: - - tasks.core.orka.ai - - gatewayclasses.gateway.orka.ai - - gateways.gateway.orka.ai - - gatewaybindings.gateway.orka.ai - resources: - - customresourcedefinitions - verbs: - - get -- apiGroups: - - core.orka.ai - resources: - - branchclaims - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - core.orka.ai - resources: - - branchclaims/status - verbs: - - get - - patch - - update -- apiGroups: - - core.orka.ai - resources: - - branchclaims/finalizers - verbs: - - update -- apiGroups: - - gateway.orka.ai - resources: - - gatewayclasses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - gateway.orka.ai - resources: - - gatewayclasses/status - verbs: - - get - - patch - - update -- apiGroups: - - gateway.orka.ai - resources: - - gatewayclasses/finalizers - verbs: - - update -- apiGroups: - - workspace.orka.ai - resources: - - executionworkspaceproviders - verbs: - - get - - list - - patch - - update - - watch -- apiGroups: - - workspace.orka.ai - resources: - - executionworkspaceproviders/status - verbs: - - get - - patch - - update -- apiGroups: - - workspace.orka.ai - resources: - - executionworkspaceproviders/finalizers - verbs: - - update ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: orka-vendor-worker-role -rules: -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch + - list + - watch - apiGroups: - core.orka.ai resources: - tools - - outboundaccesspolicies verbs: - get - list @@ -1782,460 +7968,110 @@ rules: - list --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: RoleBinding 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 + name: orka-leader-election-rolebinding + namespace: orka-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: orka-leader-election-role +subjects: +- kind: ServiceAccount + name: orka-controller-manager + namespace: orka-system --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: ClusterRoleBinding 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 + name: orka-ai-worker-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orka-ai-worker-role +subjects: +- kind: ServiceAccount + name: orka-ai-worker + namespace: orka-system --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: ClusterRoleBinding 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 + name: orka-container-worker-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orka-container-worker-role +subjects: +- kind: ServiceAccount + name: orka-container-worker + namespace: orka-system --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: ClusterRoleBinding 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 + name: orka-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orka-manager-role +subjects: +- kind: ServiceAccount + name: orka-controller-manager + namespace: orka-system --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: ClusterRoleBinding 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 + name: orka-metrics-auth-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orka-metrics-auth-role +subjects: +- kind: ServiceAccount + name: orka-controller-manager + namespace: orka-system --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: ClusterRoleBinding +metadata: + name: orka-vendor-worker-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: orka-vendor-worker-role +subjects: +- kind: ServiceAccount + name: orka-vendor-worker + namespace: orka-system +--- +apiVersion: v1 +kind: Service metadata: labels: + app.kubernetes.io/component: agent-harness-wrapper 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 + name: orka-agent-harness-wrapper + 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: 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: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-runtime-controller-rolebinding - namespace: orka-runtimes -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: orka-runtime-controller-role -subjects: -- kind: ServiceAccount - name: orka-controller-manager - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: orka-ai-worker-rolebinding - namespace: orka-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-ai-worker-role -subjects: -- kind: ServiceAccount - name: orka-ai-worker - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: orka-container-worker-rolebinding - namespace: orka-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-container-worker-role -subjects: -- kind: ServiceAccount - name: orka-container-worker - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-leader-election-rolebinding - namespace: orka-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: orka-leader-election-role -subjects: -- kind: ServiceAccount - name: orka-controller-manager - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-manager-rolebinding - namespace: orka-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-manager-role -subjects: -- kind: ServiceAccount - name: orka-controller-manager - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: orka-vendor-worker-rolebinding - namespace: orka-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-vendor-worker-role -subjects: -- kind: ServiceAccount - name: orka-vendor-worker - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-workspace-parameter-reader-rolebinding - namespace: orka-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-workspace-parameter-reader -subjects: -- kind: ServiceAccount - name: orka-controller-manager - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/component: admission - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-admission -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-admission -subjects: -- kind: ServiceAccount - name: orka-admission - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: orka-metrics-auth-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-metrics-auth-role -subjects: -- kind: ServiceAccount - name: orka-controller-manager - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-static-controller-cluster-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-static-controller-cluster-role -subjects: -- kind: ServiceAccount - name: orka-controller-manager - namespace: orka-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-v2-controller-cluster-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: orka-v2-controller-cluster-role -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 - ORKA_ACP_OPENCODE_RUNTIME_IMAGE: docker.io/sozercan/orka-acp-opencode@sha256:0000000000000000000000000000000000000000000000000000000000000000 -kind: ConfigMap -metadata: - labels: - orka.ai/acp-runtime-images: "true" - name: acp-runtime-images-2cc58tc8m7 - namespace: orka-system ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/component: admission - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-admission - namespace: orka-system -spec: - ports: - - name: https - port: 443 - protocol: TCP - targetPort: webhook - selector: - app.kubernetes.io/component: admission - app.kubernetes.io/name: orka ---- -apiVersion: v1 -kind: Service +apiVersion: v1 +kind: Service metadata: labels: app.kubernetes.io/managed-by: kustomize @@ -2260,692 +8096,226 @@ metadata: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka control-plane: controller-manager - name: orka-controller-manager-metrics-service - namespace: orka-system -spec: - ports: - - name: https - port: 8443 - protocol: TCP - targetPort: 8443 - selector: - app.kubernetes.io/name: orka - control-plane: controller-manager ---- -apiVersion: v1 -kind: Service -metadata: - 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 - resources: - requests: - storage: 1Gi ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - app.kubernetes.io/component: workspace-publisher - app.kubernetes.io/name: orka - name: orka-workspace-publisher - namespace: orka-system -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 2Gi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app.kubernetes.io/component: admission - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-admission - namespace: orka-system -spec: - replicas: 2 - selector: - matchLabels: - app.kubernetes.io/component: admission - app.kubernetes.io/name: orka - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - type: RollingUpdate - template: - metadata: - labels: - app.kubernetes.io/component: admission - app.kubernetes.io/name: orka - orka.ai/network-role: admission - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: - app.kubernetes.io/component: admission - app.kubernetes.io/name: orka - topologyKey: kubernetes.io/hostname - weight: 100 - automountServiceAccountToken: true - containers: - - args: - - --health-probe-bind-address=:8081 - - --webhook-cert-path=/var/run/orka/admission/tls - - --webhook-cert-name=tls.crt - - --webhook-cert-key=tls.key - - --webhook-service-dns-name=orka-admission.orka-system.svc - - --controller-usernames=system:serviceaccount:orka-system:orka-controller-manager,system:serviceaccount:orka-v1-system:orka-v1,system:serviceaccount:orka-v2-system:orka-v2 - - --task-provenance-trusted-users=system:serviceaccount:orka-system:orka-controller-manager,system:serviceaccount:orka-v1-system:orka-v1,system:serviceaccount:orka-v2-system:orka-v2 - - --task-provenance-trusted-service-accounts=orka-ai-worker,orka-vendor-worker,orka-v1-ai-worker,orka-v1-vendor-worker,orka-v2-ai-worker,orka-v2-vendor-worker - command: - - /orka-admission - image: docker.io/sozercan/orka@sha256:0000000000000000000000000000000000000000000000000000000000000000 - imagePullPolicy: IfNotPresent - lifecycle: - preStop: - exec: - command: - - /orka-admission - - --pre-stop-delay=5s - livenessProbe: - failureThreshold: 3 - httpGet: - path: /healthz - port: health - periodSeconds: 10 - name: admission - ports: - - containerPort: 9443 - name: webhook - protocol: TCP - - containerPort: 8081 - name: health - protocol: TCP - readinessProbe: - failureThreshold: 3 - httpGet: - path: /readyz - port: health - periodSeconds: 5 - resources: - limits: - cpu: 500m - ephemeral-storage: 128Mi - memory: 256Mi - requests: - cpu: 50m - ephemeral-storage: 32Mi - memory: 64Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 65532 - volumeMounts: - - mountPath: /var/run/orka/admission/tls - name: tls - readOnly: true - securityContext: - fsGroup: 65532 - runAsGroup: 65532 - runAsNonRoot: true - runAsUser: 65532 - seccompProfile: - type: RuntimeDefault - serviceAccountName: orka-admission - terminationGracePeriodSeconds: 30 - volumes: - - name: tls - secret: - items: - - key: tls.crt - path: tls.crt - - key: tls.key - path: tls.key - optional: false - secretName: orka-admission-tls ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - control-plane: controller-manager - name: orka-controller-manager - namespace: orka-system -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: orka - control-plane: controller-manager - strategy: - type: Recreate - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: manager - labels: - app.kubernetes.io/name: orka - control-plane: controller-manager - orka.ai/network-role: controller - spec: - containers: - - args: - - --metrics-bind-address=:8443 - - --leader-elect - - --health-probe-bind-address=:8081 - - --store-backend=sqlite - - --store-path=/data/orka.db - - --agent-execution-snapshot-key-file=/var/run/orka/agent-execution-snapshot/key - - --agent-execution-snapshot-retention=720h - - --agent-execution-snapshot-retention-interval=1h - - --controller-url=http://orka-api.orka-system.svc:8080 - - --controller-mode=harness-v2 - - --watch-namespace=orka-system - - --enforce-namespace-isolation=true - - --execution-mode-controller-usernames=system:serviceaccount:orka-system:orka-controller-manager - - --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 - - --gateway-pending-per-session=100 - - --gateway-max-records-per-gateway=1000 - - --gateway-max-rejected-records-per-gateway=250 - - --gateway-event-expiry=24h - - --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 - - --agent-sandbox-namespace-strategy=task - - --agent-sandbox-claim-timeout=2m - - --agent-sandbox-command-timeout=30m - - --agent-sandbox-cleanup-policy=delete - - --substrate-enabled=false - - --substrate-api-endpoint=api.ate-system.svc:443 - - --substrate-router-url=http://atenet-router.ate-system.svc - - --substrate-actor-dns-suffix=actors.resources.substrate.ate.dev - - --substrate-claim-timeout=2m - - --substrate-command-timeout=30m - - --substrate-cleanup-policy=delete - command: - - /manager - env: - - 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-2cc58tc8m7 - - name: ORKA_ACP_CLAUDE_RUNTIME_IMAGE - valueFrom: - configMapKeyRef: - key: ORKA_ACP_CLAUDE_RUNTIME_IMAGE - name: acp-runtime-images-2cc58tc8m7 - - name: ORKA_ACP_COPILOT_RUNTIME_IMAGE - valueFrom: - configMapKeyRef: - key: ORKA_ACP_COPILOT_RUNTIME_IMAGE - name: acp-runtime-images-2cc58tc8m7 - - name: ORKA_ACP_OPENCODE_RUNTIME_IMAGE - valueFrom: - configMapKeyRef: - key: ORKA_ACP_OPENCODE_RUNTIME_IMAGE - name: acp-runtime-images-2cc58tc8m7 - - 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 - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - name: manager - ports: [] - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - limits: - cpu: 500m - memory: 512Mi - requests: - cpu: 100m - memory: 256Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - volumeMounts: - - mountPath: /data - name: store - - mountPath: /tmp - name: tmp - - 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 - - mountPath: /var/run/orka/agent-execution-snapshot - name: agent-execution-snapshot-key - readOnly: true - securityContext: - fsGroup: 65532 - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - serviceAccountName: orka-controller-manager - terminationGracePeriodSeconds: 360 - volumes: - - name: store - persistentVolumeClaim: - claimName: orka-controller-manager-store - - emptyDir: {} - name: tmp - - 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 - - name: agent-execution-snapshot-key - secret: - defaultMode: 256 - items: - - key: snapshot-key - path: key - secretName: agent-execution-snapshot-key ---- -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 + name: orka-controller-manager-metrics-service + namespace: orka-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app.kubernetes.io/name: orka + control-plane: controller-manager +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: orka-controller-manager-store + namespace: orka-system +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi --- apiVersion: apps/v1 kind: Deployment metadata: labels: - app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/component: agent-harness-wrapper + app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-scm-egress-proxy + name: orka-agent-harness-wrapper namespace: orka-system spec: replicas: 1 selector: matchLabels: - app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/component: agent-harness-wrapper app.kubernetes.io/name: orka strategy: type: Recreate template: metadata: labels: - app.kubernetes.io/component: scm-egress-proxy + app.kubernetes.io/component: agent-harness-wrapper 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 + - 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: /healthz - port: http-proxy - name: proxy + path: /v1/health + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + name: wrapper ports: - containerPort: 8080 - name: http-proxy + name: http readinessProbe: httpGet: - path: /readyz - port: http-proxy + path: /v1/health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 resources: limits: - cpu: 500m - ephemeral-storage: 128Mi - memory: 256Mi + cpu: "2" + memory: 2Gi requests: - cpu: 25m - ephemeral-storage: 32Mi - memory: 32Mi + cpu: 100m + memory: 256Mi securityContext: allowPrivilegeEscalation: false capabilities: + add: + - SETUID + - SETGID + - CHOWN + - KILL + - FOWNER drop: - ALL readOnlyRootFilesystem: true volumeMounts: - - mountPath: /var/run/secrets/orka/scm-egress/token + - mountPath: /var/run/orka/harness-wrapper name: auth readOnly: true - subPath: token - enableServiceLinks: false + - mountPath: /var/run/orka/upload-token + name: upload-token + readOnly: true + - mountPath: /tmp + name: tmp securityContext: - fsGroup: 65532 - runAsGroup: 65532 - runAsNonRoot: true - runAsUser: 65532 + runAsGroup: 0 + runAsUser: 0 seccompProfile: type: RuntimeDefault - serviceAccountName: orka-scm-egress-proxy + serviceAccountName: orka-agent-harness-wrapper volumes: + - name: upload-token + projected: + defaultMode: 256 + sources: + - serviceAccountToken: + path: token - name: auth secret: - defaultMode: 288 - items: - - key: token - path: token - secretName: scm-egress-proxy-auth + defaultMode: 256 + secretName: harness-wrapper-auth + - emptyDir: {} + name: tmp --- apiVersion: apps/v1 kind: Deployment metadata: labels: - app.kubernetes.io/component: workspace-publisher + app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: orka - name: orka-workspace-publisher + control-plane: controller-manager + name: orka-controller-manager namespace: orka-system spec: replicas: 1 selector: matchLabels: - app.kubernetes.io/component: workspace-publisher app.kubernetes.io/name: orka + control-plane: controller-manager strategy: type: Recreate template: metadata: + annotations: + kubectl.kubernetes.io/default-container: manager labels: - app.kubernetes.io/component: workspace-publisher app.kubernetes.io/name: orka - orka.ai/network-role: workspace-publisher + control-plane: controller-manager 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 + - args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + - --store-backend=sqlite + - --store-path=/data/orka.db + - --controller-url=http://orka-api.orka-system.svc:8080 + - --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 + - --gateway-pending-per-session=100 + - --gateway-max-records-per-gateway=1000 + - --gateway-max-rejected-records-per-gateway=250 + - --gateway-event-expiry=24h + - --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 + - --agent-sandbox-namespace-strategy=task + - --agent-sandbox-claim-timeout=2m + - --agent-sandbox-command-timeout=30m + - --agent-sandbox-cleanup-policy=delete + - --substrate-enabled=false + - --substrate-api-endpoint=api.ate-system.svc:443 + - --substrate-router-url=http://atenet-router.ate-system.svc + - --substrate-actor-dns-suffix=actors.resources.substrate.ate.dev + - --substrate-claim-timeout=2m + - --substrate-command-timeout=30m + - --substrate-cleanup-policy=delete + 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 imagePullPolicy: IfNotPresent livenessProbe: httpGet: - path: /v1/health - port: http - periodSeconds: 10 - name: publisher - ports: - - containerPort: 8080 - name: http + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: [] readinessProbe: httpGet: - path: /v1/health - port: http - periodSeconds: 5 + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 resources: limits: - cpu: "2" - ephemeral-storage: 2Gi - memory: 2Gi + cpu: 500m + memory: 512Mi requests: cpu: 100m - ephemeral-storage: 512Mi memory: 256Mi securityContext: allowPrivilegeEscalation: false @@ -2955,97 +8325,28 @@ spec: readOnlyRootFilesystem: true volumeMounts: - mountPath: /data - name: data - - mountPath: /tmp/orka-workspace-publisher + name: store + - mountPath: /tmp name: tmp - - mountPath: /var/run/orka/publisher-auth/controller-token - name: publisher-auth + - mountPath: /var/run/orka/harness-wrapper + name: harness-wrapper-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 + serviceAccountName: orka-controller-manager + terminationGracePeriodSeconds: 10 volumes: - - name: data + - name: store persistentVolumeClaim: - claimName: orka-workspace-publisher - - emptyDir: - sizeLimit: 1Gi + claimName: orka-controller-manager-store + - emptyDir: {} name: tmp - - name: publisher-auth + - name: harness-wrapper-auth secret: - defaultMode: 288 - items: - - key: controller-token - path: controller-token - - key: operation-capability-secret - path: operation-capability-secret - secretName: workspace-publisher-auth ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - app.kubernetes.io/component: admission - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-admission - namespace: orka-system -spec: - minAvailable: 1 - selector: - matchLabels: - app.kubernetes.io/component: admission - app.kubernetes.io/name: orka ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicy -metadata: - name: orka-executionworkspace-core-admission.orka.ai -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 + secretName: harness-wrapper-auth --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy @@ -3101,71 +8402,6 @@ spec: name: containerWorkerServiceAccount --- apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicy -metadata: - name: orka-task-workspace-class-use.orka.ai -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 -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: orka-executionworkspace-core-admission.orka.ai -spec: - policyName: orka-executionworkspace-core-admission.orka.ai - validationActions: - - Deny ---- -apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding metadata: name: orka-gateway-task-protection @@ -3173,257 +8409,3 @@ spec: policyName: orka-gateway-task-protection validationActions: - Deny ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicyBinding -metadata: - name: orka-task-workspace-class-use.orka.ai -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 -spec: - policyName: orka-tool-workspace-class-use.orka.ai - validationActions: - - Deny ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - labels: - app.kubernetes.io/component: admission - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/name: orka - name: orka-admission - namespace: orka-system -spec: - egress: - - ports: - - port: 443 - protocol: TCP - - port: 6443 - protocol: TCP - ingress: - - ports: - - port: 9443 - protocol: TCP - - port: 8081 - protocol: TCP - podSelector: - matchLabels: - app.kubernetes.io/component: admission - app.kubernetes.io/name: orka - policyTypes: - - Ingress - - Egress ---- -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/internal/api/github_label_webhook.go b/internal/api/github_label_webhook.go index f9d18f574..ffdb84c22 100644 --- a/internal/api/github_label_webhook.go +++ b/internal/api/github_label_webhook.go @@ -742,14 +742,6 @@ func buildGitHubLabelTask(namespace, agentName, action, replayKey, delivery, eve }, Workspace: workspace, Timeout: githubTimeout(), - Env: []corev1.EnvVar{ - {Name: "ORKA_GITHUB_EVENT", Value: event}, - {Name: "ORKA_GITHUB_DELIVERY", Value: delivery}, - {Name: "ORKA_GITHUB_LABEL", Value: payload.Label.Name}, - {Name: "ORKA_GITHUB_ACTION", Value: action}, - {Name: "ORKA_GITHUB_REPOSITORY", Value: payload.Repository.FullName}, - {Name: "ORKA_GITHUB_TARGET_URL", Value: target.HTMLURL}, - }, }, } if action == githubActionReview && workspace != nil && workspace.ReadCredentialRef != nil { @@ -759,17 +751,6 @@ func buildGitHubLabelTask(namespace, agentName, action, replayKey, delivery, eve task.Labels[labels.LabelGitHubNumber] = labels.SelectorValue(strconv.Itoa(target.Number)) task.Annotations[labels.AnnotationGitHubNumber] = strconv.Itoa(target.Number) } - if target.IsPR { - if baseRepo := repoURL(target.BaseRepo); baseRepo != "" { - task.Spec.Env = append(task.Spec.Env, corev1.EnvVar{Name: workerenv.PRBaseRepo, Value: baseRepo}) - } - if target.BaseSHA != "" { - task.Spec.Env = append(task.Spec.Env, corev1.EnvVar{Name: workerenv.PRBaseSHA, Value: target.BaseSHA}) - } - } - if action == githubActionUpdateBranch { - task.Spec.Env = append(task.Spec.Env, corev1.EnvVar{Name: workerenv.AllowEmptyPushBranch, Value: "true"}) - } return task } @@ -849,7 +830,9 @@ func githubWorkspace(action string, target githubLabelTarget, replayKey string) ws.PublicationCredentialRef = &corev1alpha1.WorkspaceCredentialReference{Name: gitSecret} } } - if target.IsPR && target.BaseBranch != "" { + // prBaseBranch is a publication field: the ACP workspace preflight rejects it + // on non-write intents, and read flows carry base-branch context in the prompt. + if target.IsPR && target.BaseBranch != "" && ws.Intent == corev1alpha1.WorkspaceIntentWrite { ws.PRBaseBranch = target.BaseBranch } return ws diff --git a/internal/api/github_label_webhook_test.go b/internal/api/github_label_webhook_test.go index b396aa4ed..782c9d707 100644 --- a/internal/api/github_label_webhook_test.go +++ b/internal/api/github_label_webhook_test.go @@ -32,7 +32,6 @@ import ( "github.com/orka-agents/orka/internal/labels" "github.com/orka-agents/orka/internal/store" "github.com/orka-agents/orka/internal/store/sqlite" - "github.com/orka-agents/orka/internal/workerenv" ) const ( @@ -185,14 +184,8 @@ func TestGitHubWebhook_PullRequestUpdateBranchUsesHeadBranch(t *testing.T) { if ws.PRBaseBranch != githubWebhookTestDefaultBranch { t.Errorf("prBaseBranch = %q, want main", ws.PRBaseBranch) } - if got := githubWebhookTaskEnvValue(task.Spec.Env, workerenv.AllowEmptyPushBranch); got != "true" { - t.Errorf("%s = %q, want true", workerenv.AllowEmptyPushBranch, got) - } - if got := githubWebhookTaskEnvValue(task.Spec.Env, workerenv.PRBaseRepo); got != githubWebhookTestVekilCloneURL { - t.Errorf("%s = %q, want base repo clone URL", workerenv.PRBaseRepo, got) - } - if got := githubWebhookTaskEnvValue(task.Spec.Env, workerenv.PRBaseSHA); got != "base-sha" { - t.Errorf("%s = %q, want base-sha", workerenv.PRBaseSHA, got) + if len(task.Spec.Env) != 0 { + t.Errorf("task env = %#v, want empty because agent tasks reject arbitrary task env", task.Spec.Env) } if !strings.Contains(task.Spec.Prompt, "Update the pull request branch") { t.Errorf("prompt = %s", task.Spec.Prompt) @@ -248,8 +241,8 @@ func TestGitHubWebhook_PullRequestImplementUsesForkHeadRepo(t *testing.T) { if ws.ReadCredentialRef != nil { t.Fatalf("readCredentialRef = %#v, want nil for fork PR", ws.ReadCredentialRef) } - if ws.PRBaseBranch != githubWebhookTestDefaultBranch { - t.Errorf("prBaseBranch = %q, want main", ws.PRBaseBranch) + if ws.PRBaseBranch != "" { + t.Errorf("prBaseBranch = %q, want empty because prBaseBranch requires write workspace intent", ws.PRBaseBranch) } if !strings.Contains(task.Spec.Prompt, "Orka will not push them automatically") { t.Errorf("prompt missing no-push guidance: %s", task.Spec.Prompt) @@ -349,11 +342,11 @@ func TestGitHubWebhook_PullRequestReviewUsesInitOnlyGitSecret(t *testing.T) { if task.Annotations[labels.AnnotationWorkspaceInitContainer] != queryTrue { t.Fatalf("workspace init annotation = %q, want true", task.Annotations[labels.AnnotationWorkspaceInitContainer]) } - if got := githubWebhookTaskEnvValue(task.Spec.Env, workerenv.PRBaseRepo); got != githubWebhookTestVekilCloneURL { - t.Errorf("%s = %q, want base repo clone URL", workerenv.PRBaseRepo, got) + if ws.PRBaseBranch != "" { + t.Errorf("prBaseBranch = %q, want empty because prBaseBranch requires write workspace intent", ws.PRBaseBranch) } - if got := githubWebhookTaskEnvValue(task.Spec.Env, workerenv.PRBaseSHA); got != "base-sha" { - t.Errorf("%s = %q, want base-sha", workerenv.PRBaseSHA, got) + if len(task.Spec.Env) != 0 { + t.Errorf("task env = %#v, want empty because agent tasks reject arbitrary task env", task.Spec.Env) } } @@ -1169,15 +1162,6 @@ func readRespBody(t *testing.T, resp *http.Response) string { return buf.String() } -func githubWebhookTaskEnvValue(envVars []corev1.EnvVar, name string) string { - for _, envVar := range envVars { - if envVar.Name == name { - return envVar.Value - } - } - return "" -} - func assertNoTasks(t *testing.T, c client.Client) { t.Helper() var tasks corev1alpha1.TaskList diff --git a/internal/api/internal_caller_authorization.go b/internal/api/internal_caller_authorization.go index e6d872a5c..4701db37f 100644 --- a/internal/api/internal_caller_authorization.go +++ b/internal/api/internal_caller_authorization.go @@ -59,16 +59,27 @@ func (a internalCallerAuthorizer) verifyNamespace(c fiber.Ctx, namespace string) // ServiceAccount usernames follow the format: // system:serviceaccount::. parts := strings.Split(userInfo.Username, ":") - if len(parts) == 4 && parts[0] == "system" && parts[1] == "serviceaccount" { //nolint:goconst // "system" here is K8s SA prefix, not chat role - if parts[2] != namespace { - log.Info("cross-namespace access denied", - "callerNamespace", parts[2], - "targetNamespace", namespace, - "username", userInfo.Username, - "ip", c.IP(), - ) - return fiber.NewError(fiber.StatusForbidden, "cross-namespace access denied") - } + isServiceAccount := len(parts) == 4 && parts[0] == "system" && parts[1] == "serviceaccount" //nolint:goconst // "system" here is K8s SA prefix, not chat role + if isServiceAccount && parts[2] != namespace { + log.Info("cross-namespace access denied", + "callerNamespace", parts[2], + "targetNamespace", namespace, + "username", userInfo.Username, + "ip", c.IP(), + ) + return fiber.NewError(fiber.StatusForbidden, "cross-namespace access denied") + } + + // Fail closed: namespace-scoped internal endpoints require a verifiable + // caller namespace. Principals without one (for example non-ServiceAccount + // TokenReview identities) must not pass for arbitrary namespaces. + if userInfo.Namespace == "" && !isServiceAccount { + log.Info("internal access denied for caller without namespace identity", + "targetNamespace", namespace, + "username", userInfo.Username, + "ip", c.IP(), + ) + return fiber.NewError(fiber.StatusForbidden, "caller namespace identity required") } return nil diff --git a/internal/api/internal_caller_authorization_test.go b/internal/api/internal_caller_authorization_test.go index d9f346d46..64efdd05c 100644 --- a/internal/api/internal_caller_authorization_test.go +++ b/internal/api/internal_caller_authorization_test.go @@ -39,7 +39,10 @@ func TestInternalCallerAuthorizerVerifyNamespace(t *testing.T) { {name: "explicit namespace mismatch", userInfo: &UserInfo{Namespace: "other", Username: "system:serviceaccount:other:worker"}, wantStatus: http.StatusForbidden}, {name: "service account username mismatch", userInfo: &UserInfo{Username: "system:serviceaccount:other:worker"}, wantStatus: http.StatusForbidden}, {name: "service account namespace match", userInfo: &UserInfo{Username: "system:serviceaccount:default:worker"}, wantStatus: http.StatusNoContent}, - {name: "non service account without namespace remains allowed", userInfo: &UserInfo{Username: "admin"}, wantStatus: http.StatusNoContent}, + // Fail closed: a caller without a verifiable namespace identity must + // not pass for arbitrary namespaces. + {name: "non service account without namespace is denied", userInfo: &UserInfo{Username: "admin"}, wantStatus: http.StatusForbidden}, + {name: "explicit namespace match without service account username", userInfo: &UserInfo{Username: "admin", Namespace: "default"}, wantStatus: http.StatusNoContent}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/internal_handlers_test.go b/internal/api/internal_handlers_test.go index 745570202..ce4efa07d 100644 --- a/internal/api/internal_handlers_test.go +++ b/internal/api/internal_handlers_test.go @@ -922,7 +922,7 @@ func TestVerifyCallerNamespace(t *testing.T) { require.Equal(t, http.StatusUnauthorized, resp.StatusCode) }) - t.Run("non-SA user passes through", func(t *testing.T) { + t.Run("non-SA user without namespace identity is denied", func(t *testing.T) { app := fiber.New() app.Use(func(c fiber.Ctx) error { c.Locals(UserInfoContextKey, &UserInfo{Username: "admin"}) @@ -934,6 +934,26 @@ func TestVerifyCallerNamespace(t *testing.T) { bytes.NewReader([]byte(`{"output":"done"}`))) req.Header.Set("Content-Type", "application/json") + // Fail closed: internal endpoints are namespace-scoped, and a caller + // without a verifiable namespace identity must not pass for arbitrary + // namespaces. + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + + t.Run("namespace-scoped principal matching target is allowed", func(t *testing.T) { + app := fiber.New() + app.Use(func(c fiber.Ctx) error { + c.Locals(UserInfoContextKey, &UserInfo{Username: "admin", Namespace: "default"}) + return c.Next() + }) + app.Post("/internal/v1/results/:namespace/:taskName", h.SubmitResult) + + req := httptest.NewRequest(http.MethodPost, "/internal/v1/results/default/my-task", + bytes.NewReader([]byte(`{"output":"done"}`))) + req.Header.Set("Content-Type", "application/json") + resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) diff --git a/internal/api/monitor_handlers.go b/internal/api/monitor_handlers.go index e4ebef432..8fdd61ca9 100644 --- a/internal/api/monitor_handlers.go +++ b/internal/api/monitor_handlers.go @@ -287,6 +287,9 @@ func (h *Handlers) validateRepositoryMonitorReadOnlyAgent(c fiber.Ctx, namespace } case corev1alpha1.AgentRuntimeClaude: case corev1alpha1.AgentRuntimeCodex: + // Codex reviewers run inside the RuntimeSession boundary with + // controller-rejected elevation requests and read-intent workspace + // delta classification failing any modifying turn. if role != "reviewer" { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("%s %q runtime %q is not supported for read-only repository monitor tasks; use %s", field, ref.Name, agent.Spec.Runtime.Type, allowedRuntimes)) } diff --git a/internal/api/monitor_handlers_test.go b/internal/api/monitor_handlers_test.go index ec3582c03..84164dd84 100644 --- a/internal/api/monitor_handlers_test.go +++ b/internal/api/monitor_handlers_test.go @@ -414,6 +414,8 @@ func TestCreateRepositoryMonitor_AllowsCodexReviewer(t *testing.T) { req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) require.NoError(t, err) + // Codex reviewers run in the native read-only agent mode, so admission + // accepts them for the reviewer role. require.Equal(t, http.StatusCreated, resp.StatusCode) } diff --git a/internal/api/security_handlers.go b/internal/api/security_handlers.go index f3602d6ae..75b963f67 100644 --- a/internal/api/security_handlers.go +++ b/internal/api/security_handlers.go @@ -292,7 +292,7 @@ func (h *Handlers) createSecurityScanRun(ctx context.Context, ui *UserInfo, scan ScannerPolicyVersion: security.ScannerPolicyVersion, PolicyDigest: policy.Digest, IdempotencyKey: idempotencyKey, - StartedAt: time.Now(), + StartedAt: time.Now().UTC(), } if err := h.securityStore.CreateScanRun(ctx, run); err != nil { if errors.Is(err, store.ErrConflict) { diff --git a/internal/api/task_workspace.go b/internal/api/task_workspace.go index e25a9e865..5083979f4 100644 --- a/internal/api/task_workspace.go +++ b/internal/api/task_workspace.go @@ -83,9 +83,11 @@ func repositoryScanTaskWorkspace(scan *corev1alpha1.RepositoryScan, intent corev Ref: security.EffectiveRef(scan), ReadCredentialRef: taskWorkspaceCredentialReference(repositoryScanReadCredentialRef(scan)), SubPath: scan.Spec.SubPath, - PRBaseBranch: scan.Spec.PRBaseBranch, } if intent == corev1alpha1.WorkspaceIntentWrite { + // prBaseBranch is a publication field: the ACP workspace preflight + // rejects it on non-write intents. + workspace.PRBaseBranch = scan.Spec.PRBaseBranch workspace.PublicationGitRepo = strings.TrimSpace(scan.Spec.ForkRepo) if workspace.PublicationGitRepo == "" { workspace.PublicationGitRepo = scan.Spec.RepoURL diff --git a/internal/controller/acp_agent_configuration.go b/internal/controller/acp_agent_configuration.go index 3a5ad825d..71f8ed83d 100644 --- a/internal/controller/acp_agent_configuration.go +++ b/internal/controller/acp_agent_configuration.go @@ -193,7 +193,7 @@ func effectiveACPReasoningEffort(agent *corev1alpha1.Agent) string { return strings.ToLower(strings.TrimSpace(agent.Spec.Runtime.DefaultReasoningEffort)) } -func validateACPProviderNativePolicy(provider string, allowed, disallowed []string, allowBash bool) error { +func validateACPProviderNativePolicy(provider string, intent corev1alpha1.WorkspaceIntent, allowed, disallowed []string, allowBash bool) error { unrestricted := allowed == nil && len(disallowed) == 0 && allowBash switch provider { case string(corev1alpha1.AgentRuntimeClaude): @@ -202,6 +202,9 @@ func validateACPProviderNativePolicy(provider string, allowed, disallowed []stri if unrestricted { return nil } + if intent == corev1alpha1.WorkspaceIntentRead && codexReadOnlyNativePolicy(allowed) { + return nil + } return fmt.Errorf("codex ACP runtime cannot exactly enforce provider-native tool restrictions") case string(corev1alpha1.AgentRuntimeCopilot): for _, name := range allowed { @@ -217,6 +220,31 @@ func validateACPProviderNativePolicy(provider string, allowed, disallowed []stri } } +// codexReadOnlyNativePolicy reports whether the provider-native slice of the +// allowed tools is exactly the {Glob, Grep, Read} read-only surface. That is +// the single restricted policy codex sessions support: instead of per-tool +// switches, the RuntimeSession boundary rejects every elevation request, +// mediates file writes through the supervisor, and fails any read-intent turn +// whose workspace delta shows a modification. Brokered and custom tool names +// are ignored here because the MCP broker enforces them, not codex. +func codexReadOnlyNativePolicy(allowed []string) bool { + native := providerNativeTools[strings.ToLower(string(corev1alpha1.AgentRuntimeCodex))] + surface := make(map[string]struct{}, 3) + for _, name := range allowed { + canonical, ok := native[strings.ToLower(strings.TrimSpace(name))] + if !ok { + continue + } + switch canonical { + case providerNativeToolGlob, providerNativeToolGrep, providerNativeToolRead: + surface[canonical] = struct{}{} + default: + return false + } + } + return len(surface) == 3 +} + func validateACPProviderSystemPrompt(provider string, configuration harnessv2.AgentSessionConfiguration) error { if configuration.SystemPrompt == "" { return nil diff --git a/internal/controller/acp_mcp_policy_test.go b/internal/controller/acp_mcp_policy_test.go index c894ca39c..bf817944f 100644 --- a/internal/controller/acp_mcp_policy_test.go +++ b/internal/controller/acp_mcp_policy_test.go @@ -203,6 +203,112 @@ func TestBuildRuntimeSessionMCPConfigurationTranslatesReadOnlyOpenCodeTools(t *t } } +func TestBuildRuntimeSessionMCPConfigurationTranslatesReadOnlyPresetPerRuntime(t *testing.T) { + tests := []struct { + name string + runtime corev1alpha1.AgentRuntimeType + model *corev1alpha1.ModelConfig + images ACPRuntimeImages + wantAllowed []string + }{ + { + name: "claude", runtime: corev1alpha1.AgentRuntimeClaude, + model: &corev1alpha1.ModelConfig{Name: "model"}, + images: ACPRuntimeImages{Claude: "docker.io/example/claude@sha256:" + strings.Repeat("a", 64)}, + wantAllowed: []string{providerNativeToolGlob, providerNativeToolGrep, providerNativeToolRead}, + }, + { + name: "codex", runtime: corev1alpha1.AgentRuntimeCodex, + model: &corev1alpha1.ModelConfig{Name: "model"}, + images: ACPRuntimeImages{Codex: "docker.io/example/codex@sha256:" + strings.Repeat("b", 64)}, + wantAllowed: []string{providerNativeToolGlob, providerNativeToolGrep, providerNativeToolRead}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + task := &corev1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "review", Namespace: "default", UID: "task-uid", + Annotations: map[string]string{labels.AnnotationAgentReadOnly: scheduledRunLabelValue}, + }, + Spec: corev1alpha1.TaskSpec{ + Type: corev1alpha1.TaskTypeAgent, + AgentRuntime: &corev1alpha1.AgentRuntimeSpec{AllowedTools: readOnlyAgentAllowedTools()}, + Workspace: &corev1alpha1.WorkspaceConfig{Intent: corev1alpha1.WorkspaceIntentRead}, + }, + } + agent := &corev1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "reviewer", Namespace: "default", UID: "agent-uid", Generation: 1}, + Spec: corev1alpha1.AgentSpec{ + Model: tt.model, + Runtime: &corev1alpha1.AgentCLIRuntime{ + Type: tt.runtime, + ContractVersion: ptr.To(corev1alpha1.AgentRuntimeContractHarnessV2), + }, + }, + } + plan, err := PlanACPRuntime(task, agent, tt.images) + if err != nil { + t.Fatalf("PlanACPRuntime(read-only preset) error = %v", err) + } + configuration, err := buildRuntimeSessionMCPConfiguration(context.Background(), nil, task, agent, plan.Profile) + if err != nil { + t.Fatalf("buildRuntimeSessionMCPConfiguration(read-only preset) error = %v", err) + } + if !slices.Equal(configuration.ToolPolicy.AllowedToolNames, tt.wantAllowed) { + t.Fatalf("allowed tools = %v, want %v", configuration.ToolPolicy.AllowedToolNames, tt.wantAllowed) + } + for _, descriptor := range configuration.ToolPolicy.Tools { + if descriptor.Source != harnessv2.MCPToolSourceProviderNative { + t.Fatalf("descriptor %q source = %q, want provider_native", descriptor.Name, descriptor.Source) + } + } + }) + } +} + +func TestValidateACPProviderNativePolicyCodexReadOnlySurface(t *testing.T) { + surface := []string{providerNativeToolGlob, providerNativeToolGrep, providerNativeToolRead} + if err := validateACPProviderNativePolicy( + string(corev1alpha1.AgentRuntimeCodex), corev1alpha1.WorkspaceIntentRead, surface, nil, true, + ); err != nil { + t.Fatalf("read-intent read-only surface rejected: %v", err) + } + // The same surface must stay rejected for write intent, and any other + // restricted shape must stay rejected for every intent. + if err := validateACPProviderNativePolicy( + string(corev1alpha1.AgentRuntimeCodex), corev1alpha1.WorkspaceIntentWrite, surface, nil, true, + ); err == nil { + t.Fatal("write-intent restricted codex policy was accepted") + } + for _, allowed := range [][]string{ + {providerNativeToolGlob, providerNativeToolRead}, + {providerNativeToolGlob, providerNativeToolGrep, providerNativeToolRead, providerNativeToolWrite}, + {providerNativeToolBash}, + {}, + } { + if err := validateACPProviderNativePolicy( + string(corev1alpha1.AgentRuntimeCodex), corev1alpha1.WorkspaceIntentRead, allowed, nil, true, + ); err == nil { + t.Fatalf("restricted codex policy %v was accepted", allowed) + } + } + // Brokered tool names are enforced by the MCP broker and must not break + // the codex read-only surface (delegated children add coordination tools). + withBrokered := append([]string{"check_messages"}, surface...) + withBrokered = append(withBrokered, "send_message") + if err := validateACPProviderNativePolicy( + string(corev1alpha1.AgentRuntimeCodex), corev1alpha1.WorkspaceIntentRead, withBrokered, nil, true, + ); err != nil { + t.Fatalf("read-only surface with brokered tools rejected: %v", err) + } + if err := validateACPProviderNativePolicy( + string(corev1alpha1.AgentRuntimeCodex), corev1alpha1.WorkspaceIntentRead, nil, nil, true, + ); err != nil { + t.Fatalf("unrestricted codex policy rejected: %v", err) + } +} + func TestEffectiveACPAllowedToolsOnlyTranslatesReadOnlyOpenCodePreset(t *testing.T) { agent := &corev1alpha1.Agent{Spec: corev1alpha1.AgentSpec{Runtime: &corev1alpha1.AgentCLIRuntime{ Type: corev1alpha1.AgentRuntimeOpencode, diff --git a/internal/controller/acp_outbox.go b/internal/controller/acp_outbox.go index c5c49d64f..b629d6c42 100644 --- a/internal/controller/acp_outbox.go +++ b/internal/controller/acp_outbox.go @@ -363,9 +363,8 @@ func sanitizeOutboxError(err error) string { if err == nil { return "" } - message := strings.TrimSpace(err.Error()) - if len(message) > 1024 { - message = message[:1024] - } - return message + // The outbox store rejects invalid UTF-8, so repair invalid sequences and + // truncate on a rune boundary or the projection could never settle. + message := strings.ToValidUTF8(strings.TrimSpace(err.Error()), "�") + return truncateUTF8(message, 1024) } diff --git a/internal/controller/acp_publication.go b/internal/controller/acp_publication.go index d9b4ca2a2..a0d59f027 100644 --- a/internal/controller/acp_publication.go +++ b/internal/controller/acp_publication.go @@ -18,11 +18,12 @@ import ( "github.com/orka-agents/orka/internal/publisher" publisherservice "github.com/orka-agents/orka/internal/publisher/service" "github.com/orka-agents/orka/internal/store" + "github.com/orka-agents/orka/internal/taskterminal" ) const ( acpPublicationGeneration int64 = 1 - acpNoWorkspaceRevision = "empty" + acpNoWorkspaceRevision = taskterminal.NoWorkspaceRevision ) type acpPublicationResult struct { @@ -1334,10 +1335,17 @@ func (d *ACPDispatcher) patchDeliveryStatus(ctx context.Context, task *corev1alp } func (d *ACPDispatcher) completeSuccessWithDelivery(ctx context.Context, task *corev1alpha1.Task, status corev1alpha1.TaskDeliveryStatus, message string) error { - execution := corev1alpha1.TaskExecutionStatus{ - State: corev1alpha1.TaskExecutionStateSucceeded, Outcome: corev1alpha1.TaskExecutionOutcomeSucceeded, - Attempt: task.Status.Execution.Attempt, PromptID: task.Status.Execution.PromptID, - } + // The terminal projection must carry the complete frozen execution + // identity: prompt attempt reclamation validates the projection against + // the attempt's request digest and the Task's runtime identity, and a + // sparse payload makes the Task undeletable. Volatile fields are cleared + // so retried settlements enqueue byte-identical payloads. + execution := *task.Status.Execution.DeepCopy() + execution.State = corev1alpha1.TaskExecutionStateSucceeded + execution.Outcome = corev1alpha1.TaskExecutionOutcomeSucceeded + execution.Reason = "" + execution.Message = "" + execution.LastTransitionTime = nil if err := d.enqueueStandaloneTaskProjection(ctx, task, taskTerminalProjection{ Namespace: task.Namespace, Task: task.Name, TaskUID: string(task.UID), Attempt: execution.Attempt, Phase: corev1alpha1.TaskPhaseSucceeded, Message: message, Execution: execution, Delivery: &status, diff --git a/internal/controller/acp_runtime_profile.go b/internal/controller/acp_runtime_profile.go index 86ca6f1eb..18e99e0cb 100644 --- a/internal/controller/acp_runtime_profile.go +++ b/internal/controller/acp_runtime_profile.go @@ -101,7 +101,7 @@ func PlanACPRuntimeWithConfiguration( } allowBash := effectiveACPAllowBash(task, agent) allowed, disallowed, allowBash = normalizeACPRuntimeToolPolicy(provider, intent, allowed, disallowed, allowBash) - if err := validateACPProviderNativePolicy(provider, allowed, disallowed, allowBash); err != nil { + if err := validateACPProviderNativePolicy(provider, intent, allowed, disallowed, allowBash); err != nil { return ACPRuntimePlan{}, err } if err := validateACPProviderSystemPrompt(provider, configuration); err != nil { @@ -213,11 +213,24 @@ func effectiveACPAllowedTools(task *corev1alpha1.Task, agent *corev1alpha1.Agent values = append([]string{}, task.Spec.AgentRuntime.AllowedTools...) } if taskRequestsReadOnlyAgent(task) && taskUsesReadOnlyAgentToolPreset(task) && - agent != nil && agent.Spec.Runtime != nil && agent.Spec.Runtime.Type == corev1alpha1.AgentRuntimeOpencode { - // Repository-monitor presets use Claude-style path-scoped names. OpenCode - // cannot project those descriptors; its read-intent policy safely exposes - // only native Read and Glob (Grep is intentionally disabled). - values = []string{providerNativeToolRead, providerNativeToolGlob} + agent != nil && agent.Spec.Runtime != nil { + // Repository-monitor presets use Claude-style path-scoped names, which + // are not canonical provider-native descriptors, so translate the + // preset into the exact read-only surface each runtime can enforce. + switch agent.Spec.Runtime.Type { + case corev1alpha1.AgentRuntimeOpencode: + // OpenCode's Grep permission cannot carry the path-specific + // secret-file exclusions applied to Read, so it stays disabled. + values = []string{providerNativeToolRead, providerNativeToolGlob} + case corev1alpha1.AgentRuntimeClaude: + values = []string{providerNativeToolGlob, providerNativeToolGrep, providerNativeToolRead} + case corev1alpha1.AgentRuntimeCodex: + // Codex has no per-tool switches; this exact surface is enforced + // by the RuntimeSession boundary: elevation requests are rejected, + // file writes are supervisor-mediated, and the read-intent + // workspace delta classification fails any modifying turn. + values = []string{providerNativeToolGlob, providerNativeToolGrep, providerNativeToolRead} + } } if task != nil { _, delegatedChild := task.Labels[labels.LabelParentTask] diff --git a/internal/controller/agent_runtime_controller.go b/internal/controller/agent_runtime_controller.go index 6ceff788a..d800d225f 100644 --- a/internal/controller/agent_runtime_controller.go +++ b/internal/controller/agent_runtime_controller.go @@ -901,18 +901,12 @@ func sanitizeAgentRuntimeEndpointForStatus(endpoint string) string { func sanitizeAgentRuntimeStatusMessage(message string) string { message = events.RedactExecutionEventText(strings.TrimSpace(message)) - if len(message) > 1024 { - return message[:1024] - } - return message + return truncateUTF8(strings.ToValidUTF8(message, "�"), 1024) } func sanitizeAgentRuntimeCapabilityValue(value string) string { value = events.RedactExecutionEventText(strings.TrimSpace(value)) - if len(value) > 512 { - return value[:512] - } - return value + return truncateUTF8(strings.ToValidUTF8(value, "�"), 512) } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/controller/gateway_controller.go b/internal/controller/gateway_controller.go index 622492b28..2cdf4b8e6 100644 --- a/internal/controller/gateway_controller.go +++ b/internal/controller/gateway_controller.go @@ -790,16 +790,10 @@ func setGatewayCondition(conditions *[]metav1.Condition, conditionType string, v func sanitizeGatewayStatusMessage(message string) string { message = events.RedactExecutionEventText(strings.TrimSpace(message)) - if len(message) > 1024 { - message = message[:1024] - } - return message + return truncateUTF8(strings.ToValidUTF8(message, "�"), 1024) } func sanitizeGatewayCapability(value string) string { value = events.RedactExecutionEventText(strings.TrimSpace(value)) - if len(value) > protocol.MaxIdentityBytes { - value = value[:protocol.MaxIdentityBytes] - } - return value + return truncateUTF8(strings.ToValidUTF8(value, "�"), protocol.MaxIdentityBytes) } diff --git a/internal/controller/job_builder_test.go b/internal/controller/job_builder_test.go index c5079edce..3dc5f4798 100644 --- a/internal/controller/job_builder_test.go +++ b/internal/controller/job_builder_test.go @@ -3595,20 +3595,24 @@ func TestAddSecretVolumes_RuntimeAuthOnlyRejectsFoundryCredentials(t *testing.T) } } -func TestValidateReadOnlyAgentRuntimeRejectsCodexBecauseCredentialsAndShellCannotShareReadOnlyProfile(t *testing.T) { +func TestValidateReadOnlyAgentRuntimeAllowsCodexReadOnlyAgentMode(t *testing.T) { task := &corev1alpha1.Task{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ labels.AnnotationAgentReadOnly: scheduledRunLabelValue, }}} agent := &corev1alpha1.Agent{Spec: corev1alpha1.AgentSpec{Runtime: &corev1alpha1.AgentCLIRuntime{ Type: corev1alpha1.AgentRuntimeCodex, }}} - if err := validateReadOnlyBuiltInAgentRuntime(task, corev1alpha1.AgentRuntimeCodex); err == nil || - !strings.Contains(err.Error(), "do not support codex runtime") { - t.Fatalf("validateReadOnlyBuiltInAgentRuntime() error = %v, want fail-closed Codex rejection", err) + // Codex read-only tasks run in the native read-only agent mode with a + // kernel-enforced read-only sandbox, so validation accepts them. + if err := validateReadOnlyBuiltInAgentRuntime(task, corev1alpha1.AgentRuntimeCodex); err != nil { + t.Fatalf("validateReadOnlyBuiltInAgentRuntime() error = %v, want codex accepted", err) } - if err := validateReadOnlyAgentRuntime(task, agent); err == nil || - !strings.Contains(err.Error(), "do not support codex runtime") { - t.Fatalf("validateReadOnlyAgentRuntime() error = %v, want fail-closed Codex rejection", err) + if err := validateReadOnlyAgentRuntime(task, agent); err != nil { + t.Fatalf("validateReadOnlyAgentRuntime() error = %v, want codex accepted", err) + } + if err := validateReadOnlyBuiltInAgentRuntime(task, corev1alpha1.AgentRuntimeCopilot); err == nil || + !strings.Contains(err.Error(), "copilot runtime credentials") { + t.Fatalf("validateReadOnlyBuiltInAgentRuntime(copilot) error = %v, want fail-closed rejection", err) } } func TestValidateReadOnlyAgentRuntimeRejectsExternalRuntimeRef(t *testing.T) { diff --git a/internal/controller/repositorymonitor_controller_test.go b/internal/controller/repositorymonitor_controller_test.go index 4d9cc93b0..1198e3add 100644 --- a/internal/controller/repositorymonitor_controller_test.go +++ b/internal/controller/repositorymonitor_controller_test.go @@ -3902,9 +3902,12 @@ func assertRepositoryMonitorReviewTask(t *testing.T, ctx context.Context, cl crc if task.Spec.AgentRuntime == nil || task.Spec.Workspace == nil { t.Fatalf("task AgentRuntime.Workspace is nil") } - if task.Spec.Workspace.GitRepo != repositoryMonitorTestRepoURL || task.Spec.Workspace.Ref != "sha1" || task.Spec.Workspace.PRBaseBranch != repositoryMonitorTestDefaultBranch { + if task.Spec.Workspace.GitRepo != repositoryMonitorTestRepoURL || task.Spec.Workspace.Ref != "sha1" { t.Fatalf("workspace = %#v, want repo with exact PR head sha1", task.Spec.Workspace) } + if task.Spec.Workspace.PRBaseBranch != "" { + t.Fatalf("workspace prBaseBranch = %q, want empty because prBaseBranch requires write workspace intent", task.Spec.Workspace.PRBaseBranch) + } if task.Spec.Workspace.ReadCredentialRef == nil || task.Spec.Workspace.ReadCredentialRef.Name != "github-token" { t.Fatalf("workspace ReadCredentialRef = %#v, want github-token", task.Spec.Workspace.ReadCredentialRef) } @@ -4228,6 +4231,8 @@ func TestRepositoryMonitorValidationAllowsCodexReviewer(t *testing.T) { ) cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(reviewer).Build() reconciler := &RepositoryMonitorReconciler{Client: cl} + // Codex reviewers run in the native read-only agent mode, so monitor + // validation accepts them. reason, message, err := reconciler.validateRepositoryMonitorReviewerAgent(ctx, monitor) if err != nil || reason != "" || message != "" { t.Fatalf("validation reason=%q message=%q err=%v", reason, message, err) diff --git a/internal/controller/repositorymonitor_inventory.go b/internal/controller/repositorymonitor_inventory.go index 9ab972580..741527a34 100644 --- a/internal/controller/repositorymonitor_inventory.go +++ b/internal/controller/repositorymonitor_inventory.go @@ -382,7 +382,6 @@ func (r *RepositoryMonitorReconciler) createRepositoryMonitorReviewTask(ctx cont GitRepo: workspaceRepo, Ref: pr.HeadSHA, ReadCredentialRef: workspaceCredentialReference(readCredentialRef), - PRBaseBranch: pr.BaseBranch, }, }, } diff --git a/internal/controller/repositoryscan_controller.go b/internal/controller/repositoryscan_controller.go index c15195fec..d637de322 100644 --- a/internal/controller/repositoryscan_controller.go +++ b/internal/controller/repositoryscan_controller.go @@ -431,7 +431,7 @@ func (r *RepositoryScanReconciler) createScanRun(ctx context.Context, scan *core ScannerPolicyVersion: security.ScannerPolicyVersion, PolicyDigest: policy.Digest, IdempotencyKey: idempotencyKey, - StartedAt: time.Now(), + StartedAt: time.Now().UTC(), } if err := r.ensureScanRunRecord(ctx, run); err != nil { if errors.Is(err, store.ErrConflict) { @@ -440,7 +440,7 @@ func (r *RepositoryScanReconciler) createScanRun(ctx context.Context, scan *core return err } if err := r.Create(ctx, task); err != nil && !apierrors.IsAlreadyExists(err) { - now := time.Now() + now := time.Now().UTC() run.Phase = scanRunPhaseFailed run.CompletedAt = &now run.ErrorMessage = "scan task creation failed" @@ -491,7 +491,7 @@ func (r *RepositoryScanReconciler) hasActiveScanRun( if run.Phase == scanRunPhasePending && time.Since(run.StartedAt) < scanRunAdmissionGrace { return true, nil } - now := time.Now() + now := time.Now().UTC() run.Phase = scanRunPhaseFailed run.CompletedAt = &now run.ErrorMessage = "scan run has no active pipeline task for its idempotency key" @@ -603,7 +603,7 @@ func (r *RepositoryScanReconciler) markScanRunTerminalError(ctx context.Context, if r.SecurityStore == nil || run == nil || failure == nil { return nil } - now := time.Now() + now := time.Now().UTC() message := failure.Error() run.Phase = scanRunPhaseFailed run.CompletedAt = &now @@ -1179,7 +1179,7 @@ func (r *RepositoryScanReconciler) progressScanRunAfterMapper(ctx context.Contex } if run.Mode == scanModeIncremental && run.SliceCount > 0 && run.SkippedSliceCount == run.SliceCount { - now := time.Now() + now := time.Now().UTC() run.Phase = scanRunPhaseSucceeded run.CompletedAt = &now run.ErrorMessage = "" @@ -1195,7 +1195,7 @@ func (r *RepositoryScanReconciler) progressScanRunAfterMapper(ctx context.Contex return true, nil } - now := time.Now() + now := time.Now().UTC() run.Phase = scanRunPhaseSucceeded run.CompletedAt = &now run.ErrorMessage = "" @@ -1582,10 +1582,7 @@ func conciseTaskMessage(message, fallback string) string { if line == "" { continue } - if len(line) > 512 { - line = line[:512] - } - return line + return truncateUTF8(line, 512) } return fallback } diff --git a/internal/controller/runtime_pool_controller.go b/internal/controller/runtime_pool_controller.go index e9c56bf2e..12a2f584b 100644 --- a/internal/controller/runtime_pool_controller.go +++ b/internal/controller/runtime_pool_controller.go @@ -2765,10 +2765,7 @@ func validSHA256Digest(value string) bool { func sanitizeRuntimePoolMessage(message string) string { message = events.RedactExecutionEventText(strings.TrimSpace(message)) - if len(message) > 1024 { - message = message[:1024] - } - return message + return truncateUTF8(strings.ToValidUTF8(message, "�"), 1024) } func sanitizeRuntimePoolReason(reason string) string { diff --git a/internal/controller/substrate_actor_pool_controller.go b/internal/controller/substrate_actor_pool_controller.go index 39a2f6c97..d65ff01c3 100644 --- a/internal/controller/substrate_actor_pool_controller.go +++ b/internal/controller/substrate_actor_pool_controller.go @@ -241,10 +241,7 @@ func (r *SubstrateActorPoolReconciler) updateSubstrateActorPoolStatus( func sanitizeSubstrateActorPoolMessage(message string) string { message = strings.TrimSpace(message) - if len(message) > 1024 { - return message[:1024] - } - return message + return truncateUTF8(strings.ToValidUTF8(message, "�"), 1024) } func validateSubstrateActorPoolTargetActors(poolNamespace, poolName string, targetActors int32, allowZero bool) error { diff --git a/internal/controller/task_controller.go b/internal/controller/task_controller.go index 505b7ec23..f58341e0b 100644 --- a/internal/controller/task_controller.go +++ b/internal/controller/task_controller.go @@ -3336,12 +3336,15 @@ func validateReadOnlyBuiltInAgentRuntime(task *corev1alpha1.Task, runtimeType co return nil } switch runtimeType { - case corev1alpha1.AgentRuntimeCodex: - return fmt.Errorf("read-only agent tasks do not support codex runtime because Codex requires shell access while model credentials are exposed") case corev1alpha1.AgentRuntimeCopilot: return fmt.Errorf("read-only agent tasks do not support copilot runtime credentials because GITHUB_TOKEN can mutate GitHub") - default: + // Codex is supported: read-only tasks run inside the RuntimeSession + // boundary with controller-rejected elevation requests, + // supervisor-mediated file writes, and read-intent workspace delta + // classification failing any modifying turn, with the same + // per-session loopback provider credential the other runtimes + // receive. return nil } } diff --git a/internal/controller/task_workspace_credentials.go b/internal/controller/task_workspace_credentials.go index 7da5478ad..170dcf2d0 100644 --- a/internal/controller/task_workspace_credentials.go +++ b/internal/controller/task_workspace_credentials.go @@ -37,9 +37,11 @@ func repositoryScanTaskWorkspace(scan *corev1alpha1.RepositoryScan, intent corev Ref: security.EffectiveRef(scan), ReadCredentialRef: workspaceCredentialReference(repositoryScanReadCredentialRef(scan)), SubPath: scan.Spec.SubPath, - PRBaseBranch: scan.Spec.PRBaseBranch, } if intent == corev1alpha1.WorkspaceIntentWrite { + // prBaseBranch is a publication field: the ACP workspace preflight + // rejects it on non-write intents. + workspace.PRBaseBranch = scan.Spec.PRBaseBranch workspace.PublicationGitRepo = strings.TrimSpace(scan.Spec.ForkRepo) if workspace.PublicationGitRepo == "" { workspace.PublicationGitRepo = scan.Spec.RepoURL diff --git a/internal/harness/v2/conformance/check.go b/internal/harness/v2/conformance/check.go index 1421edc22..2ed0a696b 100644 --- a/internal/harness/v2/conformance/check.go +++ b/internal/harness/v2/conformance/check.go @@ -14,6 +14,7 @@ import ( "slices" "strings" "time" + "unicode/utf8" harnessv2 "github.com/orka-agents/orka/internal/harness/v2" ) @@ -418,9 +419,15 @@ func boundedMessage(err error) string { if err == nil { return "" } - message := strings.TrimSpace(err.Error()) - if len(message) > 1024 { - return message[:1024] + // Repair invalid sequences and truncate on a rune boundary so bounded + // messages always remain valid UTF-8. + message := strings.ToValidUTF8(strings.TrimSpace(err.Error()), "�") + if len(message) <= 1024 { + return message } - return message + end := 1024 + for end > 0 && !utf8.RuneStart(message[end]) { + end-- + } + return message[:end] } diff --git a/internal/providerproxy/providerproxy.go b/internal/providerproxy/providerproxy.go new file mode 100644 index 000000000..b186a808e --- /dev/null +++ b/internal/providerproxy/providerproxy.go @@ -0,0 +1,227 @@ +// Package providerproxy holds the security-critical request/response +// filtering shared by Orka's provider-facing HTTP proxies: the standalone +// orka-provider-auth-proxy binary and the ACP supervisor's per-session +// provider proxy. Both proxies sit between untrusted callers and provider +// credentials, so the header allow/deny decisions, path-traversal checks, +// upstream target construction, and response byte bounds live here exactly +// once — a hardening fix in this package reaches every proxy. +package providerproxy + +import ( + "errors" + "io" + "net/http" + "net/url" + "strings" +) + +const ( + authorizationHeader = "Authorization" + proxyAuthorizationHeader = "Proxy-Authorization" + apiKeyHeader = "X-Api-Key" + legacyAPIKeyHeader = "Api-Key" + contentEncodingHeader = "Content-Encoding" +) + +// ErrResponseTooLarge reports an upstream response body that exceeds the +// proxy's configured byte limit. +var ErrResponseTooLarge = errors.New("provider upstream response exceeds limit") + +// HasUnsafePathSegment reports whether a proxied URL path must be rejected +// before it is joined onto the upstream base URL. It applies the strictest +// union of the checks the two provider proxies previously implemented +// independently — every path either proxy rejected before is still rejected: +// +// - any "." or ".." path segment, checked on both the raw path and its +// percent-decoded form, so percent-encoded traversal ("%2e%2e") cannot +// survive a later decode upstream; +// - any backslash or NUL byte, again in both raw and decoded forms, so +// Windows-style separators and string-truncation tricks cannot smuggle +// traversal past the segment check; +// - malformed percent-encoding, which is rejected outright because it +// cannot be normalized safely. +func HasUnsafePathSegment(path string) bool { + if hasUnsafeRawPath(path) { + return true + } + decoded, err := url.PathUnescape(path) + if err != nil { + return true + } + return hasUnsafeRawPath(decoded) +} + +func hasUnsafeRawPath(path string) bool { + if strings.ContainsAny(path, "\\\x00") { + return true + } + for segment := range strings.SplitSeq(path, "/") { + if segment == "." || segment == ".." { + return true + } + } + return false +} + +// Target joins a proxied request path and raw query onto the upstream base +// URL. Callers must reject unsafe paths with HasUnsafePathSegment first. +func Target(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 +} + +// CopyRequestHeaders copies caller request headers onto the upstream request, +// dropping hop-by-hop headers, headers nominated by Connection, and headers +// that could leak caller credentials, identity, or routing metadata to the +// provider. Header values are never logged or printed here. +func CopyRequestHeaders(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) + } + } +} + +// CopyResponseHeaders copies upstream response headers back to the caller, +// dropping hop-by-hop headers, headers nominated by Connection, and headers +// that could leak provider credentials or redirect the caller elsewhere. +func CopyResponseHeaders(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) + } + } +} + +// HasDisallowedContentEncoding reports whether the header declares a +// Content-Encoding other than identity. The proxies refuse compressed bodies +// in both directions so byte limits apply to the true payload size. +func HasDisallowedContentEncoding(header http.Header) bool { + encoding := strings.TrimSpace(header.Get(contentEncodingHeader)) + return encoding != "" && !strings.EqualFold(encoding, "identity") +} + +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, apiKeyHeader, legacyAPIKeyHeader, + "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", contentEncodingHeader, "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, apiKeyHeader, legacyAPIKeyHeader, + "Set-Cookie", "Set-Cookie2", "Location", "Server", "Alt-Svc", "Www-Authenticate", "Proxy-Authenticate", + contentEncodingHeader: + return true + default: + return false + } +} + +// StreamBoundedResponse copies the upstream response body to the caller, +// enforcing the byte limit as it streams. It never writes more than limit +// bytes: the read that would cross the limit returns ErrResponseTooLarge +// without forwarding the offending chunk, and reads never run more than one +// byte past the limit. When flusher is non-nil it is flushed after every +// write so streamed (for example server-sent event) responses propagate +// promptly; pass nil to keep the destination's default buffering. +func StreamBoundedResponse(destination io.Writer, source io.Reader, limit int64, flusher http.Flusher) 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 { + return ErrResponseTooLarge + } + if n > 0 { + written, writeErr := destination.Write(buffer[:n]) + remaining -= int64(written) + if writeErr != nil { + return writeErr + } + if written != n { + return io.ErrShortWrite + } + if flusher != nil { + flusher.Flush() + } + } + if readErr != nil { + if errors.Is(readErr, io.EOF) { + return nil + } + return readErr + } + } +} + +// TryAcquireSlot attempts to take one request slot without blocking. +func TryAcquireSlot(slots chan struct{}) bool { + select { + case slots <- struct{}{}: + return true + default: + return false + } +} + +// ReleaseSlot returns a request slot taken with TryAcquireSlot. +func ReleaseSlot(slots chan struct{}) { + <-slots +} + +// WriteError writes a plain-text, non-cacheable proxy error response. The +// message must never contain credential material. +func WriteError(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") +} diff --git a/internal/providerproxy/providerproxy_test.go b/internal/providerproxy/providerproxy_test.go new file mode 100644 index 000000000..e9419f24a --- /dev/null +++ b/internal/providerproxy/providerproxy_test.go @@ -0,0 +1,381 @@ +package providerproxy + +import ( + "bytes" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func TestHasUnsafePathSegment(t *testing.T) { + for _, test := range []struct { + name string + path string + unsafe bool + }{ + {name: "empty", path: "", unsafe: false}, + {name: "root", path: "/", unsafe: false}, + {name: "benign api path", path: "/v1/messages", unsafe: false}, + {name: "benign nested path", path: "/base/v1/chat/completions", unsafe: false}, + {name: "dots inside a segment", path: "/v1.2/models.json", unsafe: false}, + {name: "segment starting with dots", path: "/..hidden/x", unsafe: false}, + {name: "benign query-free asterisk", path: "*", unsafe: false}, + {name: "raw parent traversal", path: "/../secrets", unsafe: true}, + {name: "raw parent traversal mid-path", path: "/v1/../admin", unsafe: true}, + {name: "raw parent traversal at end", path: "/v1/..", unsafe: true}, + {name: "bare parent segment", path: "..", unsafe: true}, + {name: "raw current-directory segment", path: "/./v1", unsafe: true}, + {name: "bare current-directory segment", path: ".", unsafe: true}, + {name: "percent-encoded parent traversal", path: "/%2e%2e/secrets", unsafe: true}, + {name: "percent-encoded parent traversal uppercase", path: "/v1/%2E%2E", unsafe: true}, + {name: "percent-encoded current-directory segment", path: "/%2e/v1", unsafe: true}, + {name: "percent-encoded slash traversal", path: "/a%2f..%2fb", unsafe: true}, + {name: "mixed encoded parent segment", path: "/.%2e/x", unsafe: true}, + {name: "raw backslash", path: "/a\\b", unsafe: true}, + {name: "raw windows traversal", path: "/a\\..\\b", unsafe: true}, + {name: "percent-encoded backslash", path: "/a%5Cb", unsafe: true}, + {name: "percent-encoded backslash lowercase", path: "/a%5cb", unsafe: true}, + {name: "raw NUL byte", path: "/a\x00b", unsafe: true}, + {name: "percent-encoded NUL byte", path: "/a%00b", unsafe: true}, + {name: "malformed percent encoding", path: "/a%zzb", unsafe: true}, + {name: "truncated percent encoding", path: "/a%2", unsafe: true}, + } { + t.Run(test.name, func(t *testing.T) { + if got := HasUnsafePathSegment(test.path); got != test.unsafe { + t.Fatalf("HasUnsafePathSegment(%q) = %t, want %t", test.path, got, test.unsafe) + } + }) + } +} + +func TestCopyRequestHeadersDropsSensitiveAndHopByHopHeaders(t *testing.T) { + source := http.Header{} + for name, value := range map[string]string{ + "Authorization": "Bearer caller-credential", + "Proxy-Authorization": "Bearer proxy-credential", + "X-Api-Key": "caller-key", + "Api-Key": "caller-legacy-key", + "Cookie": "caller=cookie", + "Set-Cookie": "caller=cookie", + "Forwarded": "for=203.0.113.1", + "X-Forwarded-For": "203.0.113.1", + "X-Forwarded-Anything": "anything", + "X-Real-Ip": "203.0.113.1", + "X-Original-Url": "/original", + "X-Rewrite-Url": "/rewrite", + "X-Envoy-Original-Path": "/envoy", + "X-Http-Method-Override": "DELETE", + "Txn-Token": "transaction", + "Origin": "http://caller.example", + "Referer": "http://caller.example/page", + "Openai-Organization": "org", + "Openai-Project": "project", + "Anthropic-Organization-Id": "org", + "Traceparent": "00-trace", + "Tracestate": "vendor=1", + "Baggage": "key=value", + "Content-Encoding": "gzip", + "Expect": "100-continue", + "X-Orka-Internal": "internal", + "Sec-Fetch-Site": "cross-site", + "Keep-Alive": "timeout=5", + "Te": "trailers", + "Trailer": "Expires", + "Transfer-Encoding": "chunked", + "Upgrade": "websocket", + "Proxy-Connection": "keep-alive", + "Proxy-Authenticate": "Basic", + "X-Connection-Named": "remove-me", + } { + source.Set(name, value) + } + source.Set("Connection", "X-Connection-Named") + source.Add("Content-Type", "application/json") + source.Add("Accept", "application/json") + source.Add("Accept", "text/event-stream") + source.Set("Anthropic-Version", "2023-06-01") + source.Set("Openai-Beta", "assistants=v2") + source["x-lowercase-safe"] = []string{"kept"} + + destination := http.Header{} + CopyRequestHeaders(destination, source) + + for _, name := range []string{ + "Authorization", "Proxy-Authorization", "X-Api-Key", "Api-Key", "Cookie", "Set-Cookie", + "Forwarded", "X-Forwarded-For", "X-Forwarded-Anything", "X-Real-Ip", "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", "Content-Encoding", "Expect", "X-Orka-Internal", + "Sec-Fetch-Site", "Connection", "Keep-Alive", "Te", "Trailer", "Transfer-Encoding", + "Upgrade", "Proxy-Connection", "Proxy-Authenticate", "X-Connection-Named", + } { + if values := destination.Values(name); len(values) != 0 { + t.Errorf("sensitive or hop-by-hop request header %s was forwarded: %q", name, values) + } + } + if got := destination.Get("Content-Type"); got != "application/json" { + t.Errorf("Content-Type = %q, want application/json", got) + } + if got := destination.Values("Accept"); len(got) != 2 || got[0] != "application/json" || got[1] != "text/event-stream" { + t.Errorf("Accept values = %q, want both preserved in order", got) + } + if got := destination.Get("Anthropic-Version"); got != "2023-06-01" { + t.Errorf("Anthropic-Version = %q, want preserved", got) + } + if got := destination.Get("Openai-Beta"); got != "assistants=v2" { + t.Errorf("Openai-Beta = %q, want preserved", got) + } + if got := destination.Get("X-Lowercase-Safe"); got != "kept" { + t.Errorf("non-canonical source header was not canonicalized and preserved: %q", got) + } +} + +func TestCopyRequestHeadersDropsNonCanonicalSensitiveNames(t *testing.T) { + source := http.Header{ + "authorization": []string{"Bearer caller-credential"}, + "x-api-key": []string{"caller-key"}, + "cookie": []string{"caller=cookie"}, + } + destination := http.Header{} + CopyRequestHeaders(destination, source) + if len(destination) != 0 { + t.Fatalf("non-canonical sensitive request headers were forwarded: %v", destination) + } +} + +func TestCopyResponseHeadersDropsSensitiveAndHopByHopHeaders(t *testing.T) { + source := http.Header{} + for name, value := range map[string]string{ + "Authorization": "Bearer provider-credential", + "Proxy-Authorization": "Bearer provider-proxy-credential", + "X-Api-Key": "provider-key", + "Api-Key": "provider-legacy-key", + "Set-Cookie": "provider=secret", + "Set-Cookie2": "provider=secret", + "Location": "http://elsewhere.example", + "Server": "provider-server", + "Alt-Svc": `h3=":443"`, + "Www-Authenticate": "Bearer", + "Proxy-Authenticate": "Basic", + "Content-Encoding": "gzip", + "Transfer-Encoding": "chunked", + "Keep-Alive": "timeout=5", + "Upgrade": "websocket", + "X-Connection-Named": "remove-me", + } { + source.Set(name, value) + } + source.Set("Connection", "X-Connection-Named") + source.Set("Content-Type", "application/json") + source.Set("X-Request-Id", "request-1") + source.Set("Openai-Version", "2020-10-01") + + destination := http.Header{} + CopyResponseHeaders(destination, source) + + for _, name := range []string{ + "Authorization", "Proxy-Authorization", "X-Api-Key", "Api-Key", "Set-Cookie", "Set-Cookie2", + "Location", "Server", "Alt-Svc", "Www-Authenticate", "Proxy-Authenticate", "Content-Encoding", + "Transfer-Encoding", "Keep-Alive", "Upgrade", "Connection", "X-Connection-Named", + } { + if values := destination.Values(name); len(values) != 0 { + t.Errorf("sensitive or hop-by-hop response header %s was forwarded: %q", name, values) + } + } + for name, want := range map[string]string{ + "Content-Type": "application/json", + "X-Request-Id": "request-1", + "Openai-Version": "2020-10-01", + } { + if got := destination.Get(name); got != want { + t.Errorf("%s = %q, want %q", name, got, want) + } + } +} + +func TestHasDisallowedContentEncoding(t *testing.T) { + for _, test := range []struct { + name string + encoding string + disallowed bool + }{ + {name: "absent", encoding: "", disallowed: false}, + {name: "identity", encoding: "identity", disallowed: false}, + {name: "identity uppercase", encoding: "IDENTITY", disallowed: false}, + {name: "identity padded", encoding: " identity ", disallowed: false}, + {name: "gzip", encoding: "gzip", disallowed: true}, + {name: "brotli", encoding: "br", disallowed: true}, + {name: "gzip padded", encoding: " gzip ", disallowed: true}, + } { + t.Run(test.name, func(t *testing.T) { + header := http.Header{} + if test.encoding != "" { + header.Set("Content-Encoding", test.encoding) + } + if got := HasDisallowedContentEncoding(header); got != test.disallowed { + t.Fatalf("HasDisallowedContentEncoding(%q) = %t, want %t", test.encoding, got, test.disallowed) + } + }) + } +} + +func TestTarget(t *testing.T) { + for _, test := range []struct { + name string + base string + requestPath string + rawQuery string + want string + }{ + {name: "root base", base: "http://upstream.example/", requestPath: "/v1/messages", want: "http://upstream.example/v1/messages"}, + {name: "prefixed base", base: "http://upstream.example/base", requestPath: "/v1/responses", rawQuery: "stream=true", want: "http://upstream.example/base/v1/responses?stream=true"}, + {name: "prefixed base with trailing slash", base: "http://upstream.example/base/", requestPath: "/v1/models", want: "http://upstream.example/base/v1/models"}, + {name: "root suffix", base: "http://upstream.example/base", requestPath: "/", want: "http://upstream.example/base/"}, + } { + t.Run(test.name, func(t *testing.T) { + base, err := url.Parse(test.base) + if err != nil { + t.Fatal(err) + } + target := Target(base, test.requestPath, test.rawQuery) + if target.String() != test.want { + t.Fatalf("Target(%q, %q, %q) = %q, want %q", test.base, test.requestPath, test.rawQuery, target.String(), test.want) + } + if target.RawPath != "" { + t.Fatalf("Target left RawPath = %q, want empty", target.RawPath) + } + if base.Path != mustParse(t, test.base).Path { + t.Fatal("Target mutated the shared base URL") + } + }) + } +} + +func mustParse(t *testing.T, raw string) *url.URL { + t.Helper() + parsed, err := url.Parse(raw) + if err != nil { + t.Fatal(err) + } + return parsed +} + +type chunkReader struct { + data []byte + chunk int +} + +func (r *chunkReader) Read(buffer []byte) (int, error) { + if len(r.data) == 0 { + return 0, io.EOF + } + n := min(min(len(buffer), r.chunk), len(r.data)) + copy(buffer, r.data[:n]) + r.data = r.data[n:] + return n, nil +} + +type countingFlusher struct{ flushes int } + +func (f *countingFlusher) Flush() { f.flushes++ } + +func TestStreamBoundedResponse(t *testing.T) { + t.Run("under limit", func(t *testing.T) { + var out bytes.Buffer + if err := StreamBoundedResponse(&out, strings.NewReader("hello"), 10, nil); err != nil { + t.Fatalf("StreamBoundedResponse() = %v, want nil", err) + } + if out.String() != "hello" { + t.Fatalf("streamed %q, want %q", out.String(), "hello") + } + }) + + t.Run("exactly at limit", func(t *testing.T) { + var out bytes.Buffer + if err := StreamBoundedResponse(&out, strings.NewReader("12345"), 5, nil); err != nil { + t.Fatalf("StreamBoundedResponse() = %v, want nil", err) + } + if out.String() != "12345" { + t.Fatalf("streamed %q, want %q", out.String(), "12345") + } + }) + + t.Run("over limit in one read", func(t *testing.T) { + var out bytes.Buffer + err := StreamBoundedResponse(&out, strings.NewReader("123456"), 5, nil) + if !errors.Is(err, ErrResponseTooLarge) { + t.Fatalf("StreamBoundedResponse() = %v, want ErrResponseTooLarge", err) + } + if int64(out.Len()) > 5 { + t.Fatalf("streamed %d bytes, want at most the 5-byte limit", out.Len()) + } + }) + + t.Run("over limit across chunks", func(t *testing.T) { + var out bytes.Buffer + err := StreamBoundedResponse(&out, &chunkReader{data: []byte("1234567890"), chunk: 3}, 5, nil) + if !errors.Is(err, ErrResponseTooLarge) { + t.Fatalf("StreamBoundedResponse() = %v, want ErrResponseTooLarge", err) + } + if out.String() != "123" { + t.Fatalf("streamed %q, want the chunks that fit under the limit", out.String()) + } + }) + + t.Run("zero limit rejects any body", func(t *testing.T) { + var out bytes.Buffer + err := StreamBoundedResponse(&out, strings.NewReader("x"), 0, nil) + if !errors.Is(err, ErrResponseTooLarge) { + t.Fatalf("StreamBoundedResponse() = %v, want ErrResponseTooLarge", err) + } + if out.Len() != 0 { + t.Fatalf("streamed %q, want nothing", out.String()) + } + }) + + t.Run("flushes after every chunk", func(t *testing.T) { + var out bytes.Buffer + flusher := &countingFlusher{} + if err := StreamBoundedResponse(&out, &chunkReader{data: []byte("abcdef"), chunk: 2}, 10, flusher); err != nil { + t.Fatalf("StreamBoundedResponse() = %v, want nil", err) + } + if out.String() != "abcdef" || flusher.flushes != 3 { + t.Fatalf("streamed %q with %d flushes, want %q with 3 flushes", out.String(), flusher.flushes, "abcdef") + } + }) +} + +func TestTryAcquireAndReleaseSlot(t *testing.T) { + slots := make(chan struct{}, 1) + if !TryAcquireSlot(slots) { + t.Fatal("first acquire failed on an empty slot channel") + } + if TryAcquireSlot(slots) { + t.Fatal("second acquire succeeded past capacity") + } + ReleaseSlot(slots) + if !TryAcquireSlot(slots) { + t.Fatal("acquire failed after release") + } +} + +func TestWriteError(t *testing.T) { + recorder := httptest.NewRecorder() + WriteError(recorder, http.StatusBadGateway, "provider upstream request failed") + if recorder.Code != http.StatusBadGateway { + t.Fatalf("status = %d, want %d", recorder.Code, http.StatusBadGateway) + } + if got := recorder.Header().Get("Content-Type"); got != "text/plain; charset=utf-8" { + t.Fatalf("Content-Type = %q", got) + } + if got := recorder.Header().Get("Cache-Control"); got != "no-store" { + t.Fatalf("Cache-Control = %q", got) + } + if recorder.Body.String() != "provider upstream request failed\n" { + t.Fatalf("body = %q", recorder.Body.String()) + } +} diff --git a/internal/safesymlink/symlink.go b/internal/safesymlink/symlink.go index 33c8c6c3d..0632e536c 100644 --- a/internal/safesymlink/symlink.go +++ b/internal/safesymlink/symlink.go @@ -40,54 +40,67 @@ func Resolve(linkPath, target string, maxPathBytes, maxTargetBytes int) (string, // path set. It rejects cycles and any non-directory archive entry nested below // a symlink path, preventing extraction through a link. func ValidateGraph(paths map[string]struct{}, links map[string]string, maxPathBytes, maxTargetBytes int) error { + resolutions := make(map[string]string, len(links)) for linkPath, target := range links { if _, ok := paths[linkPath]; !ok { return fmt.Errorf("symlink %q is absent from the path inventory", linkPath) } - if _, err := Resolve(linkPath, target, maxPathBytes, maxTargetBytes); err != nil { + resolved, err := Resolve(linkPath, target, maxPathBytes, maxTargetBytes) + if err != nil { return err } - prefix := linkPath + "/" + resolutions[linkPath] = resolved + } + if len(links) > 0 { + // An entry is nested below a link exactly when the bytes before one + // of its "/" separators equal a link path, so walking each entry's + // ancestors costs O(paths × depth) instead of O(links × paths). for entryPath := range paths { - if strings.HasPrefix(entryPath, prefix) { - return fmt.Errorf("archive entry %q is nested below symlink %q", entryPath, linkPath) + for index := range len(entryPath) { + if entryPath[index] != '/' { + continue + } + if _, ok := links[entryPath[:index]]; ok { + return fmt.Errorf("archive entry %q is nested below symlink %q", entryPath, entryPath[:index]) + } } } } - for linkPath, target := range links { - resolved, err := Resolve(linkPath, target, maxPathBytes, maxTargetBytes) - if err != nil { - return err - } - if err := resolveGraph(linkPath, resolved, links, maxPathBytes, maxTargetBytes, map[string]struct{}{linkPath: {}}); err != nil { + for linkPath, resolved := range resolutions { + if err := resolveGraph(linkPath, resolved, resolutions, maxPathBytes, map[string]struct{}{linkPath: {}}); err != nil { return err } } return nil } -func resolveGraph(start, value string, links map[string]string, maxPathBytes, maxTargetBytes int, visiting map[string]struct{}) error { - prefix, remainder, found := firstLinkPrefix(value, links) +// maxSymlinkChainDepth mirrors the Linux SYMLOOP_MAX resolution bound: chains +// nested deeper than this can never resolve at use time, and the cap keeps +// graph validation linear in the number of links instead of quadratic. +const maxSymlinkChainDepth = 40 + +func resolveGraph(start, value string, resolutions map[string]string, maxPathBytes int, visiting map[string]struct{}) error { + prefix, remainder, found := firstLinkPrefix(value, resolutions) if !found { return nil } if _, cycle := visiting[prefix]; cycle { return fmt.Errorf("symlink %q participates in a cycle", start) } + if len(visiting) >= maxSymlinkChainDepth { + return fmt.Errorf("symlink %q chain exceeds the maximum resolution depth", start) + } visiting[prefix] = struct{}{} defer delete(visiting, prefix) - resolved, err := Resolve(prefix, links[prefix], maxPathBytes, maxTargetBytes) - if err != nil { - return err - } + resolved := resolutions[prefix] if remainder != "" { resolved = path.Clean(path.Join(resolved, remainder)) if err := validatePath(resolved, maxPathBytes); err != nil { return fmt.Errorf("symlink %q chain is unsafe: %w", start, err) } } - return resolveGraph(start, resolved, links, maxPathBytes, maxTargetBytes, visiting) + return resolveGraph(start, resolved, resolutions, maxPathBytes, visiting) } func firstLinkPrefix(value string, links map[string]string) (string, string, bool) { diff --git a/internal/safesymlink/symlink_test.go b/internal/safesymlink/symlink_test.go index e116e50fc..8394aaae6 100644 --- a/internal/safesymlink/symlink_test.go +++ b/internal/safesymlink/symlink_test.go @@ -1,6 +1,10 @@ package safesymlink -import "testing" +import ( + "fmt" + "strings" + "testing" +) func TestValidateGraph(t *testing.T) { t.Parallel() @@ -26,7 +30,10 @@ func TestValidateGraphRejectsUnsafeLinks(t *testing.T) { {name: "absolute", paths: map[string]struct{}{"link": {}}, links: map[string]string{"link": "/etc/passwd"}}, {name: "escape", paths: map[string]struct{}{"dir/link": {}}, links: map[string]string{"dir/link": "../../outside"}}, {name: "cycle", paths: map[string]struct{}{"a": {}, "b": {}}, links: map[string]string{"a": "b", "b": "a"}}, + {name: "chain cycle", paths: map[string]struct{}{"a": {}, "b": {}, "c": {}}, links: map[string]string{"a": "b", "b": "c", "c": "a"}}, {name: "nested entry", paths: map[string]struct{}{"link": {}, "link/file": {}}, links: map[string]string{"link": "target"}}, + {name: "link below link", paths: map[string]struct{}{"a": {}, "a/b": {}}, links: map[string]string{"a": "target", "a/b": "other"}}, + {name: "trailing separator entry", paths: map[string]struct{}{"link": {}, "link/": {}}, links: map[string]string{"link": "target"}}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -36,3 +43,87 @@ func TestValidateGraphRejectsUnsafeLinks(t *testing.T) { }) } } + +func TestValidateGraphPrefixBoundaries(t *testing.T) { + t.Parallel() + paths := map[string]struct{}{ + "a/b": {}, + "a/bc": {}, + "ab": {}, + "A/file": {}, + } + links := map[string]string{"a/b": "c"} + if err := ValidateGraph(paths, links, 4096, 4096); err != nil { + t.Fatalf("entries sharing a link's byte prefix were rejected: %v", err) + } +} + +func TestValidateGraphAcceptsDuplicateTargets(t *testing.T) { + t.Parallel() + paths := map[string]struct{}{"first": {}, "second": {}} + links := map[string]string{"first": "shared", "second": "shared"} + if err := ValidateGraph(paths, links, 4096, 4096); err != nil { + t.Fatalf("links sharing one resolved target were rejected: %v", err) + } +} + +func TestValidateGraphAcceptsChainWithRemainder(t *testing.T) { + t.Parallel() + paths := map[string]struct{}{"chain": {}, "hop": {}} + links := map[string]string{"chain": "hop/tail", "hop": "real"} + if err := ValidateGraph(paths, links, 4096, 4096); err != nil { + t.Fatalf("safe chained graph rejected: %v", err) + } +} + +func TestValidateGraphBoundsChainDepth(t *testing.T) { + t.Parallel() + buildChain := func(length int) (map[string]struct{}, map[string]string) { + paths := make(map[string]struct{}, length+1) + links := make(map[string]string, length) + for index := range length { + name := fmt.Sprintf("link-%04d", index) + next := fmt.Sprintf("link-%04d", index+1) + if index == length-1 { + next = "end" + } + paths[name] = struct{}{} + links[name] = next + } + paths["end"] = struct{}{} + return paths, links + } + + paths, links := buildChain(maxSymlinkChainDepth - 1) + if err := ValidateGraph(paths, links, 4096, 4096); err != nil { + t.Fatalf("chain within the depth bound rejected: %v", err) + } + + // Chains deeper than the kernel's resolution bound can never resolve at + // use time and would otherwise cost O(links x depth) validation work. + paths, links = buildChain(maxSymlinkChainDepth + 1) + err := ValidateGraph(paths, links, 4096, 4096) + if err == nil || !strings.Contains(err.Error(), "maximum resolution depth") { + t.Fatalf("over-deep chain error = %v, want maximum resolution depth rejection", err) + } +} + +func BenchmarkValidateGraph(b *testing.B) { + paths := make(map[string]struct{}, 22000) + links := make(map[string]string, 2000) + for index := range 20000 { + paths[fmt.Sprintf("src/pkg%03d/file%05d.go", index%200, index)] = struct{}{} + } + for index := range 2000 { + linkPath := fmt.Sprintf("node_modules/mod%04d/link", index) + paths[linkPath] = struct{}{} + links[linkPath] = fmt.Sprintf("../../vendor/mod%04d", index) + } + b.ReportAllocs() + + for b.Loop() { + if err := ValidateGraph(paths, links, 4096, 4096); err != nil { + b.Fatalf("ValidateGraph() error = %v", err) + } + } +} diff --git a/internal/store/sqlite/security_store.go b/internal/store/sqlite/security_store.go index d4d65a454..52ec6c397 100644 --- a/internal/store/sqlite/security_store.go +++ b/internal/store/sqlite/security_store.go @@ -61,9 +61,26 @@ func unmarshalSecurityJSON(payload string, value any) error { return json.Unmarshal([]byte(payload), value) } +// Timestamps persist as TEXT and list queries order them lexicographically, so +// every persisted time is normalized to UTC here regardless of the location +// callers supplied. +func utcTime(value time.Time) time.Time { + return value.UTC() +} + +func utcTimePtr(value *time.Time) *time.Time { + if value == nil { + return nil + } + utc := value.UTC() + return &utc +} + // CreateScanRun inserts a new scan run. func (s *Store) CreateScanRun(ctx context.Context, run *store.ScanRun) error { - now := time.Now() + run.StartedAt = utcTime(run.StartedAt) + run.CompletedAt = utcTimePtr(run.CompletedAt) + now := time.Now().UTC() if run.StartedAt.IsZero() { run.StartedAt = now } @@ -110,6 +127,8 @@ func (s *Store) CreateScanRun(ctx context.Context, run *store.ScanRun) error { // UpdateScanRun updates a scan run. func (s *Store) UpdateScanRun(ctx context.Context, run *store.ScanRun) error { + run.StartedAt = utcTime(run.StartedAt) + run.CompletedAt = utcTimePtr(run.CompletedAt) _, err := s.db.ExecContext(ctx, `UPDATE security_scan_runs SET task_name = ?, mode = ?, phase = ?, base_commit = ?, head_commit = ?, commit_count = ?, @@ -246,7 +265,9 @@ func (s *Store) UpsertReviewSlice(ctx context.Context, slice *store.ReviewSlice) return err } - now := time.Now() + slice.CreatedAt = utcTime(slice.CreatedAt) + slice.LastReviewedAt = utcTimePtr(slice.LastReviewedAt) + now := time.Now().UTC() if slice.CreatedAt.IsZero() { slice.CreatedAt = now } @@ -411,7 +432,7 @@ func (s *Store) GetReviewSlice(ctx context.Context, namespace, repositoryScan, i // UpdateReviewSliceStatus updates slice status and review timestamp. func (s *Store) UpdateReviewSliceStatus(ctx context.Context, namespace, repositoryScan, id, lastScanRunID, status string) error { - now := time.Now() + now := time.Now().UTC() res, err := s.db.ExecContext(ctx, `UPDATE security_review_slices SET status = ?, last_reviewed_at = CASE WHEN ? IN ('reviewed', 'completed') THEN ? ELSE last_reviewed_at END, @@ -480,7 +501,8 @@ func (s *Store) SaveThreatModel(ctx context.Context, model *store.ThreatModel) e model.Version = latestVersion + 1 } - now := time.Now() + model.CreatedAt = utcTime(model.CreatedAt) + now := time.Now().UTC() if model.CreatedAt.IsZero() { model.CreatedAt = now } @@ -539,7 +561,8 @@ func (s *Store) UpsertFinding(ctx context.Context, finding *store.Finding) error return err } - now := time.Now() + finding.CreatedAt = utcTime(finding.CreatedAt) + now := time.Now().UTC() if finding.CreatedAt.IsZero() { finding.CreatedAt = now } @@ -843,7 +866,8 @@ func (s *Store) CreatePatchProposal(ctx context.Context, proposal *store.PatchPr if proposal.PublicationEvidence != nil { return store.ValidationErrorf("patch proposal publication evidence must be bound with BindPatchProposalPublicationEvidence") } - now := time.Now() + proposal.CreatedAt = utcTime(proposal.CreatedAt) + now := time.Now().UTC() if proposal.CreatedAt.IsZero() { proposal.CreatedAt = now } @@ -886,7 +910,7 @@ func (s *Store) BindPatchProposalPublicationEvidence(ctx context.Context, propos return fmt.Errorf("%w: patch proposal %s/%s publication evidence already differs", store.ErrConflict, proposal.Namespace, proposal.ID) } - now := time.Now() + now := time.Now().UTC() result, err := tx.ExecContext(ctx, `UPDATE security_patch_proposals SET branch = ?, diff_artifact = ?, summary_artifact = ?, status = ?, pr_number = ?, pr_url = ?, @@ -1062,7 +1086,7 @@ func (s *Store) UpdatePatchProposal(ctx context.Context, proposal *store.PatchPr if proposal.PublicationEvidence != nil { return store.ValidationErrorf("patch proposal publication evidence must be bound with BindPatchProposalPublicationEvidence") } - now := time.Now() + now := time.Now().UTC() result, err := s.db.ExecContext(ctx, `UPDATE security_patch_proposals SET task_name = ?, branch = ?, diff_artifact = ?, summary_artifact = ?, status = ?, pr_number = ?, pr_url = ?, updated_at = ? @@ -1148,11 +1172,12 @@ func (s *Store) CreateDroppedFinding(ctx context.Context, dropped *store.Dropped dropped.SliceID, dropped.Reason, dropped.SampleJSON, - time.Now().Format(time.RFC3339Nano), + time.Now().UTC().Format(time.RFC3339Nano), }, "|")) } + dropped.CreatedAt = utcTime(dropped.CreatedAt) if dropped.CreatedAt.IsZero() { - dropped.CreatedAt = time.Now() + dropped.CreatedAt = time.Now().UTC() } _, err := s.db.ExecContext(ctx, `INSERT OR IGNORE INTO security_dropped_findings diff --git a/internal/taskterminal/projection.go b/internal/taskterminal/projection.go index 68bf3eb5f..aac6a0265 100644 --- a/internal/taskterminal/projection.go +++ b/internal/taskterminal/projection.go @@ -17,6 +17,12 @@ const ( // ProjectionKind is the durable outbox projection for terminal Task status. ProjectionKind = "TaskTerminalStatus" + // NoWorkspaceRevision is the protocol-only revision harness v2 records for + // Tasks without a repository workspace. It is not a Git object ID: the + // outbox projector strips it from delivery evidence before the + // schema-validated Task status, while immutable projection payloads keep it. + NoWorkspaceRevision = "empty" + restoreIdentityChangedReason = corev1alpha1.TaskExecutionReason("RestoreIdentityChanged") ) @@ -144,7 +150,7 @@ func validateProjection( if projection.Phase != terminalPhase(wantState, attempt.DeliveryState) { return nil, conflict("restored terminal projection phase does not match its terminal outcome") } - if task.Status.Delivery == nil || !equalDeliveryEvidence(projection.Delivery, task.Status.Delivery) { + if task.Status.Delivery == nil || !equalDeliveryEvidence(task, projection.Delivery, task.Status.Delivery) { return nil, conflict("restored terminal projection delivery evidence does not match its Task") } if err := validateRuntimeIdentity(projection.Execution, *task.Status.Execution, *attempt); err != nil { @@ -182,11 +188,16 @@ func validateFinalizedSessionTurn( return conflict("finalized SessionTurn identity is invalid") } sourceTaskUID = strings.TrimSpace(sourceTaskUID) + // turn.Key.LeaseGeneration is the per-prompt Session mutation-lease + // generation and is fenced against the PromptAttempt's recorded lease. + // Task.Status.Execution.RuntimeSessionGeneration is the RuntimeSession + // incarnation generation — a different counter that only coincides with + // the lease generation for a session's first prompt — so the turn binds to + // the Task through the session UID, never through that generation. if turn.Key.TaskUID != sourceTaskUID || turn.Key.Attempt != attempt.Key.Attempt || turn.Key.PromptID != attempt.Key.PromptID || turn.PromptAttemptID != attempt.ID || turn.Key.SessionUID != attempt.SessionUID || turn.Key.LeaseGeneration != attempt.SessionLeaseGeneration || - turn.Key.SessionUID != task.Status.Execution.RuntimeSessionUID || - turn.Key.LeaseGeneration != task.Status.Execution.RuntimeSessionGeneration { + turn.Key.SessionUID != task.Status.Execution.RuntimeSessionUID { return conflict("finalized SessionTurn does not match its Task and PromptAttempt") } wantTerminalKind := store.SessionTurnOutcomeMarker @@ -253,7 +264,7 @@ func terminalPhase(state corev1alpha1.TaskExecutionState, delivery store.PromptD return corev1alpha1.TaskPhaseFailed } -func equalDeliveryEvidence(left, right *corev1alpha1.TaskDeliveryStatus) bool { +func equalDeliveryEvidence(task *corev1alpha1.Task, left, right *corev1alpha1.TaskDeliveryStatus) bool { if left == nil || right == nil { return left == nil && right == nil } @@ -262,6 +273,17 @@ func equalDeliveryEvidence(left, right *corev1alpha1.TaskDeliveryStatus) bool { leftCopy.State, rightCopy.State = "", "" leftCopy.Outcome, rightCopy.Outcome = "", "" leftCopy.LastTransitionTime, rightCopy.LastTransitionTime = nil, nil + // The projector strips the protocol-only no-workspace revision before the + // schema-validated Task status while the immutable payload keeps it, so + // evidence is compared through that same normalization. + if task != nil && task.Spec.Workspace == nil { + if leftCopy.StartingSHA == NoWorkspaceRevision { + leftCopy.StartingSHA = "" + } + if rightCopy.StartingSHA == NoWorkspaceRevision { + rightCopy.StartingSHA = "" + } + } return reflect.DeepEqual(leftCopy, rightCopy) } @@ -279,8 +301,12 @@ func validateRuntimeIdentity(projected, task corev1alpha1.TaskExecutionStatus, a if attempt.RuntimeInstanceID != "" && projected.RuntimeInstanceID != attempt.RuntimeInstanceID { return conflict("restored terminal projection runtime instance does not match its source attempt") } - if attempt.SessionUID != "" && (projected.RuntimeSessionUID != attempt.SessionUID || - projected.RuntimeSessionGeneration != attempt.SessionLeaseGeneration) { + // The projection records the RuntimeSession incarnation generation while + // the attempt records the per-prompt Session mutation-lease generation; + // they only coincide for a session's first prompt, so the attempt binding + // is fenced by session UID here and by lease generation on the finalized + // SessionTurn. + if attempt.SessionUID != "" && projected.RuntimeSessionUID != attempt.SessionUID { return conflict("restored terminal projection runtime Session does not match its source attempt") } return nil diff --git a/internal/taskterminal/projection_test.go b/internal/taskterminal/projection_test.go index a06bc02c5..8d5e43cf4 100644 --- a/internal/taskterminal/projection_test.go +++ b/internal/taskterminal/projection_test.go @@ -76,6 +76,73 @@ func TestValidateFinalizedSessionProjectionAcceptsPinnedLegacySparseExecution(t } } +func TestValidateFinalizedSessionProjectionAcceptsContinuationLeaseGeneration(t *testing.T) { + // A session continuation prompt runs under a later mutation-lease + // generation than the RuntimeSession incarnation generation frozen in the + // Task execution identity. Reclamation must still validate, or every + // continuation Task becomes undeletable. + task, sourceUID, attempt, projection := restoredProjectionFixture() + task.Spec.SessionRef = &corev1alpha1.SessionReference{Name: "session-transcript", Create: false} + attempt.SessionLeaseGeneration = task.Status.Execution.RuntimeSessionGeneration + 1 + payload := marshalProjection(t, projection) + turn := finalizedSessionProjectionTurn(t, payload, attempt) + + if _, err := ValidateFinalizedSessionProjection(payload, task, sourceUID, attempt, turn); err != nil { + t.Fatalf("ValidateFinalizedSessionProjection(continuation lease) error = %v", err) + } +} + +func TestValidateFinalizedSessionProjectionAcceptsStrippedNoWorkspaceRevision(t *testing.T) { + // A Task without a repository workspace freezes the protocol-only "empty" + // revision in its projected delivery evidence, while the outbox projector + // strips that value before the schema-validated Task status. Reclamation + // must compare through the same normalization, or every no-workspace Task + // with delivery evidence becomes undeletable. + task, sourceUID, attempt, projection := restoredProjectionFixture() + task.Spec.SessionRef = &corev1alpha1.SessionReference{Name: "session-transcript", Create: true} + attempt.DeliveryState = store.PromptDeliveryReadValidated + projection.Delivery = &corev1alpha1.TaskDeliveryStatus{ + State: corev1alpha1.TaskDeliveryStateReadValidated, + Outcome: corev1alpha1.TaskDeliveryOutcomeReadValidated, + StartingSHA: NoWorkspaceRevision, + } + task.Status.Delivery = &corev1alpha1.TaskDeliveryStatus{ + State: corev1alpha1.TaskDeliveryStateReadValidated, + Outcome: corev1alpha1.TaskDeliveryOutcomeReadValidated, + } + payload := marshalProjection(t, projection) + turn := finalizedSessionProjectionTurn(t, payload, attempt) + + if _, err := ValidateFinalizedSessionProjection(payload, task, sourceUID, attempt, turn); err != nil { + t.Fatalf("ValidateFinalizedSessionProjection(no-workspace revision) error = %v", err) + } +} + +func TestValidateRestoredProjectionRejectsNoWorkspaceRevisionForWorkspaceTask(t *testing.T) { + task, sourceUID, attempt, projection := restoredProjectionFixture() + task.Spec.Workspace = &corev1alpha1.WorkspaceConfig{} + projection.Delivery.StartingSHA = NoWorkspaceRevision + task.Status.Delivery.StartingSHA = "" + + if _, err := ValidateRestoredProjection(marshalProjection(t, projection), task, sourceUID, attempt); !errors.Is(err, store.ErrConflict) { + t.Fatalf("ValidateRestoredProjection(workspace task with protocol revision) error = %v, want ErrConflict", err) + } +} + +func TestValidateFinalizedSessionProjectionRejectsLeaseGenerationDrift(t *testing.T) { + task, sourceUID, attempt, projection := restoredProjectionFixture() + task.Spec.SessionRef = &corev1alpha1.SessionReference{Name: "session-transcript", Create: false} + payload := marshalProjection(t, projection) + turn := finalizedSessionProjectionTurn(t, payload, attempt) + // Drift between the finalized turn and its attempt must stay fenced even + // though the Task's incarnation generation is no longer compared. + attempt.SessionLeaseGeneration++ + + if _, err := ValidateFinalizedSessionProjection(payload, task, sourceUID, attempt, turn); !errors.Is(err, store.ErrConflict) { + t.Fatalf("ValidateFinalizedSessionProjection(lease drift) error = %v, want ErrConflict", err) + } +} + func TestValidateFinalizedSessionProjectionRejectsUnpinnedOrPartialLegacyPayload(t *testing.T) { tests := []struct { name string diff --git a/internal/tools/chat_helpers.go b/internal/tools/chat_helpers.go index 9eff7377f..ff162882e 100644 --- a/internal/tools/chat_helpers.go +++ b/internal/tools/chat_helpers.go @@ -8,10 +8,13 @@ package tools import ( "fmt" + "strconv" "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" + + corev1alpha1 "github.com/orka-agents/orka/api/v1alpha1" ) // classifyChatK8sErr returns a ChatToolResult for common K8s API errors. @@ -85,6 +88,36 @@ func chatGetIntArg(args map[string]any, key string, defaultVal int) int { } } +// chatParseBoolArg parses a bool tool argument that may arrive as a JSON +// boolean or a string boolean. +func chatParseBoolArg(value any) (bool, error) { + switch v := value.(type) { + case bool: + return v, nil + case string: + return strconv.ParseBool(strings.TrimSpace(v)) + default: + return false, fmt.Errorf("value is not a boolean") + } +} + +// chatParseIntArg parses an integer tool argument that may arrive as a JSON +// number or a numeric string. +func chatParseIntArg(value any) (int, error) { + switch v := value.(type) { + case float64: + return int(v), nil + case int: + return v, nil + case int64: + return int(v), nil + case string: + return strconv.Atoi(strings.TrimSpace(v)) + default: + return 0, fmt.Errorf("value is not an integer") + } +} + // chatGetStringSliceArg extracts a string slice argument. func chatGetStringSliceArg(args map[string]any, key string) []string { v, ok := args[key] @@ -123,6 +156,47 @@ func parseTimeoutArg(args map[string]any) (time.Duration, string, bool) { return d, "", true } +// parseMaxTurnsArg parses the optional maxTurns argument and returns an error +// result if invalid. +func parseMaxTurnsArg(args map[string]any) (*int32, string, bool) { + raw, ok := args["maxTurns"] + if !ok { + return nil, "", true + } + turns, err := chatParseIntArg(raw) + if err != nil { + r, _ := ChatToolErrorResult("invalid_arguments", + "maxTurns must be an integer", + "Provide maxTurns as a positive integer or omit it") + return nil, r, false + } + value := int32(turns) + return &value, "", true +} + +// workspaceRequestsPublication reports whether any publication field upgrades +// the workspace to write intent. +func workspaceRequestsPublication(wsCfg *corev1alpha1.WorkspaceConfig) bool { + return wsCfg.PublicationGitRepo != "" || wsCfg.PushBranch != "" || wsCfg.PRBaseBranch != "" || wsCfg.CreatePR +} + +// parseCreatePRArg parses the optional workspace.createPR argument and returns +// an error result if invalid. +func parseCreatePRArg(wsMap map[string]any) (bool, string, bool) { + raw, ok := wsMap["createPR"] + if !ok { + return false, "", true + } + createPR, err := chatParseBoolArg(raw) + if err != nil { + r, _ := ChatToolErrorResult("invalid_arguments", + "workspace.createPR must be a boolean", + "Set createPR to true or false") + return false, r, false + } + return createPR, "", true +} + // taskCreatedMsg returns the appropriate message for a created task. func taskCreatedMsg(schedule string) string { if schedule != "" { diff --git a/internal/tools/create_agent_task.go b/internal/tools/create_agent_task.go index 11f1871ea..2136d3b35 100644 --- a/internal/tools/create_agent_task.go +++ b/internal/tools/create_agent_task.go @@ -101,12 +101,10 @@ func (t *CreateAgentTaskTool) Execute(ctx context.Context, args json.RawMessage) var agentRuntime *corev1alpha1.AgentRuntimeSpec - if maxTurns, ok := a["maxTurns"]; ok { - if agentRuntime == nil { - agentRuntime = &corev1alpha1.AgentRuntimeSpec{} - } - mt := int32(maxTurns.(float64)) - agentRuntime.MaxTurns = &mt + if turns, errResult, ok := parseMaxTurnsArg(a); !ok { + return errResult, nil + } else if turns != nil { + agentRuntime = &corev1alpha1.AgentRuntimeSpec{MaxTurns: turns} } if ws, ok := a[workspaceField]; ok { @@ -130,8 +128,12 @@ func (t *CreateAgentTaskTool) Execute(ctx context.Context, args json.RawMessage) wsCfg.PublicationGitRepo = chatGetStringArg(wsMap, "publicationGitRepo") wsCfg.PushBranch = chatGetStringArg(wsMap, "pushBranch") wsCfg.PRBaseBranch = chatGetStringArg(wsMap, "prBaseBranch") - wsCfg.CreatePR, _ = wsMap["createPR"].(bool) - if wsCfg.PublicationGitRepo != "" || wsCfg.PushBranch != "" || wsCfg.PRBaseBranch != "" || wsCfg.CreatePR { + createPR, errResult, ok := parseCreatePRArg(wsMap) + if !ok { + return errResult, nil + } + wsCfg.CreatePR = createPR + if workspaceRequestsPublication(wsCfg) { wsCfg.Intent = corev1alpha1.WorkspaceIntentWrite } publicationCredential := strings.TrimSpace(chatGetStringArg(wsMap, "publicationCredentialRef")) diff --git a/internal/tools/create_agent_task_test.go b/internal/tools/create_agent_task_test.go index 32efb3cf1..c6a1899f7 100644 --- a/internal/tools/create_agent_task_test.go +++ b/internal/tools/create_agent_task_test.go @@ -304,6 +304,82 @@ func TestCreateAgentTaskTool_Execute_RejectsNonObjectWorkspace(t *testing.T) { } } +func TestCreateAgentTaskTool_Execute_ParsesStringBooleanCreatePR(t *testing.T) { + fc := newFakeClient() + ctx := newCreateAgentTaskToolCtx(fc) + tool := &CreateAgentTaskTool{} + args := json.RawMessage(`{ + "prompt":"Fix the bug", + "agentRef":"codex-agent", + "workspace":{ + "gitRepo":"https://github.com/example/source.git", + "pushBranch":"orka/fix", + "prBaseBranch":"main", + "createPR":"true", + "readCredentialRef":"source-read", + "publicationCredentialRef":"target-write", + "forgeCredentialRef":"forge" + } + }`) + + result, err := tool.Execute(ctx, args) + if err != nil { + t.Fatalf("Execute() error = %v", err) + } + var response ChatToolResult + if err := json.Unmarshal([]byte(result), &response); err != nil { + t.Fatal(err) + } + if !response.Success { + t.Fatalf("response = %#v, want success", response) + } + task := &corev1alpha1.Task{} + if err := fc.Get(t.Context(), apitypes.NamespacedName{Name: testAgentTaskGeneratedName, Namespace: defaultNamespace}, task); err != nil { + t.Fatal(err) + } + if task.Spec.Workspace == nil || !task.Spec.Workspace.CreatePR { + t.Fatalf("workspace = %#v, want createPR true from string boolean", task.Spec.Workspace) + } +} + +func TestCreateAgentTaskTool_Execute_RejectsInvalidCreatePRAndMaxTurns(t *testing.T) { + tests := []struct { + name string + args string + want string + }{ + { + name: "createPR", + args: `{"prompt":"Fix","agentRef":"codex-agent","workspace":{"gitRepo":"https://github.com/example/repo","createPR":"yes-please"}}`, + want: "workspace.createPR must be a boolean", + }, + { + name: "maxTurns", + args: `{"prompt":"Fix","agentRef":"codex-agent","maxTurns":"lots"}`, + want: "maxTurns must be an integer", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fc := newFakeClient() + ctx := newCreateAgentTaskToolCtx(fc) + tool := &CreateAgentTaskTool{} + + result, err := tool.Execute(ctx, json.RawMessage(tt.args)) + if err != nil { + t.Fatalf("Execute() error = %v", err) + } + var response ChatToolResult + if err := json.Unmarshal([]byte(result), &response); err != nil { + t.Fatal(err) + } + if response.Success || response.ErrorType != errTypeInvalidArgs || !strings.Contains(response.Error, tt.want) { + t.Fatalf("response = %#v, want %q", response, tt.want) + } + }) + } +} + func TestCreateAgentTaskTool_Execute_RequiresExplicitPublicationCredentialForWrite(t *testing.T) { fc := newFakeClient() ctx := newCreateAgentTaskToolCtx(fc) diff --git a/manifest_staging/charts/orka/templates/rbac.yaml b/manifest_staging/charts/orka/templates/rbac.yaml index 19f5c045a..c4ffe2fa8 100644 --- a/manifest_staging/charts/orka/templates/rbac.yaml +++ b/manifest_staging/charts/orka/templates/rbac.yaml @@ -31,6 +31,15 @@ rules: - apiGroups: ["workspace.orka.ai"] resources: ["executionworkspaces", "executionworkspaceclasses", "executionworkspacepools"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # The workspace admission policies gate spec.coreAdmission writes on the + # "admit" verb and class-backed Task/Tool creation on the "use" verb, so the + # controller needs both or fail-closed admission rejects its own writes. + - apiGroups: ["workspace.orka.ai"] + resources: ["executionworkspaces"] + verbs: ["admit"] + - apiGroups: ["workspace.orka.ai"] + resources: ["executionworkspaceclasses"] + verbs: ["use"] - apiGroups: ["workspace.orka.ai"] resources: ["executionworkspaces/status", "executionworkspaceclasses/status", "executionworkspacepools/status"] verbs: ["get", "update", "patch"] diff --git a/manifest_staging/charts/orka/templates/workspace-class-use-policy.yaml b/manifest_staging/charts/orka/templates/workspace-class-use-policy.yaml new file mode 100644 index 000000000..1a8c61ec7 --- /dev/null +++ b/manifest_staging/charts/orka/templates/workspace-class-use-policy.yaml @@ -0,0 +1,72 @@ +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/manifest_staging/charts/orka/templates/workspace-core-admission-policy.yaml b/manifest_staging/charts/orka/templates/workspace-core-admission-policy.yaml new file mode 100644 index 000000000..47dd68f2a --- /dev/null +++ b/manifest_staging/charts/orka/templates/workspace-core-admission-policy.yaml @@ -0,0 +1,44 @@ +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/scripts/lib/live-acp-runtime-kind-bootstrap.sh b/scripts/lib/live-acp-runtime-kind-bootstrap.sh index 44cd0f03a..c95d5796f 100644 --- a/scripts/lib/live-acp-runtime-kind-bootstrap.sh +++ b/scripts/lib/live-acp-runtime-kind-bootstrap.sh @@ -65,8 +65,15 @@ live_acp_kind_preflight() { [[ -x "${LIVE_ACP_KINDCTL_BIN}" ]] || live_acp_kind_die "kindctl is not executable: ${LIVE_ACP_KINDCTL_BIN}" || return 1 [[ -x "${LIVE_ACP_VEKIL_DEPLOY_SCRIPT}" ]] || live_acp_kind_die "Vekil deploy script is not executable: ${LIVE_ACP_VEKIL_DEPLOY_SCRIPT}" || return 1 [[ -x "${LIVE_ACP_VALIDATOR_SCRIPT}" ]] || live_acp_kind_die "live ACP validator is not executable: ${LIVE_ACP_VALIDATOR_SCRIPT}" || return 1 - [[ "${LIVE_ACP_VEKIL_IMAGE}" =~ @sha256:[0-9a-f]{64}$ ]] || \ - live_acp_kind_die "LIVE_ACP_VEKIL_IMAGE must be digest-pinned" || return 1 + if [[ -n "${LIVE_ACP_VEKIL_LOCAL_IMAGE:-}" ]]; then + # A locally built Vekil is published through the run's own registry and + # digest-pinned there, so development builds stay immutable end to end. + docker image inspect "${LIVE_ACP_VEKIL_LOCAL_IMAGE}" >/dev/null 2>&1 || \ + live_acp_kind_die "LIVE_ACP_VEKIL_LOCAL_IMAGE is not a local Docker image: ${LIVE_ACP_VEKIL_LOCAL_IMAGE}" || return 1 + else + [[ "${LIVE_ACP_VEKIL_IMAGE}" =~ @sha256:[0-9a-f]{64}$ ]] || \ + live_acp_kind_die "LIVE_ACP_VEKIL_IMAGE must be digest-pinned" || return 1 + fi [[ -n "${COPILOT_GITHUB_TOKEN:-}" ]] || \ live_acp_kind_die "COPILOT_GITHUB_TOKEN is required; this bootstrap is noninteractive and never starts device-code login" || return 1 [[ "${ACP_E2E_OPENCODE_CONTEXT_WINDOW:-}" =~ ^[1-9][0-9]*$ ]] || \ @@ -97,13 +104,22 @@ live_acp_kind_create_cluster() { export LIVE_ACP_CONTEXT LIVE_ACP_KIND_CLUSTER LIVE_ACP_KUBECONFIG } -live_acp_kind_build_and_publish_images() { +live_acp_kind_start_registry() { # shellcheck source=scripts/lib/kind-local-registry.sh . "${LIVE_ACP_REPO_ROOT}/scripts/lib/kind-local-registry.sh" orka_kind_registry_start "${LIVE_ACP_KIND_CLUSTER}" LIVE_ACP_REGISTRY_STARTED=1 + if [[ -n "${LIVE_ACP_VEKIL_LOCAL_IMAGE:-}" ]]; then + # Publish the development Vekil before the Vekil deploy step so the + # deployment references the digest-pinned copy in the run's registry. + LIVE_ACP_VEKIL_IMAGE="$(orka_kind_registry_push "${LIVE_ACP_VEKIL_LOCAL_IMAGE}" vekil/vekil)" + export LIVE_ACP_VEKIL_IMAGE + fi +} + +live_acp_kind_build_and_publish_images() { live_acp_kind_run make -C "${LIVE_ACP_REPO_ROOT}" \ IMG="${LIVE_ACP_CONTROLLER_IMAGE}" \ ACP_CODEX_RUNTIME_IMG="${LIVE_ACP_CODEX_IMAGE}" \ @@ -298,9 +314,11 @@ live_acp_kind_probe_vekil_wire_path() { ) ;; /chat/completions) + # Reasoning-family chat models reject the deprecated max_tokens + # parameter, so the probe uses max_completion_tokens. payload="$(jq -cn --arg model "${model}" '{ model:$model, - max_tokens:16, + max_completion_tokens:16, stream:true, messages:[{role:"user",content:"Reply with exactly OK."}] }')" @@ -482,6 +500,7 @@ live_acp_kind_create_release_credentials() { live_acp_kind_bootstrap() { live_acp_kind_create_cluster + live_acp_kind_start_registry live_acp_kind_deploy_vekil live_acp_kind_build_and_publish_images live_acp_kind_deploy_orka diff --git a/scripts/live-acp-runtime-e2e.sh b/scripts/live-acp-runtime-e2e.sh index 55c5b0903..1d1a94d49 100755 --- a/scripts/live-acp-runtime-e2e.sh +++ b/scripts/live-acp-runtime-e2e.sh @@ -343,6 +343,7 @@ temp_root="$(mktemp -d "${TMPDIR:-/tmp}/orka-acp-e2e.XXXXXX")" chmod 700 "${temp_root}" namespace_create_attempted=0 namespace_created=0 +namespace_shared=0 namespace_uid="" api_forward_pid="" api_token_file="${temp_root}/api-token" @@ -927,13 +928,13 @@ delete_test_branchclaims() { delete_test_namespace_now() { recover_namespace_ownership || return 1 - [[ "${namespace_created}" -eq 1 ]] || return 0 + [[ "${namespace_created}" -eq 1 || "${namespace_shared}" -eq 1 ]] || return 0 probe_namespace "${namespace}" || return 1 if [[ "${namespace_probe_state}" == "absent" ]]; then namespace_created=0 return 0 fi - if ! jq -e --arg run "${run_id}" --arg uid "${namespace_uid}" ' + if [[ "${namespace_created}" -eq 1 ]] && ! jq -e --arg run "${run_id}" --arg uid "${namespace_uid}" ' .metadata.uid == $uid and .metadata.labels["orka.ai/acp-e2e-run"] == $run and .metadata.labels["app.kubernetes.io/managed-by"] == "live-acp-runtime-e2e" @@ -953,6 +954,11 @@ delete_test_namespace_now() { fi delete_test_branchclaims "${owners_file}" || return 1 + if [[ "${namespace_shared}" -eq 1 ]]; then + log "Leaving shared namespace ${namespace} in place after run-resource cleanup" + return 0 + fi + log "Cleaning up ACP e2e namespace ${namespace}" if ! k delete namespace "${namespace}" --ignore-not-found=true --wait=false >/dev/null; then warn "failed to request namespace/${namespace} deletion" @@ -987,10 +993,10 @@ cleanup() { fi if [[ "${keep_resources}" == "1" || "${remote_cleanup_preserve}" == "1" ]]; then - if [[ "${namespace_created}" -eq 1 ]]; then + if [[ "${namespace_created}" -eq 1 || "${namespace_shared}" -eq 1 ]]; then warn "preserving namespace ${namespace}" fi - elif [[ "${namespace_created}" -eq 1 ]]; then + elif [[ "${namespace_created}" -eq 1 || "${namespace_shared}" -eq 1 ]]; then if ! delete_test_namespace_now; then cleanup_rc=1 fi @@ -1046,7 +1052,7 @@ dump_diagnostics() { log "Failure diagnostics (Secret contents and task results are intentionally excluded)" run_redacted k get nodes -o wide || true run_redacted k -n "${orka_namespace}" get deployment,pod,service,persistentvolumeclaim -o wide || true - if [[ "${namespace_created}" -eq 1 ]]; then + if [[ "${namespace_created}" -eq 1 || "${namespace_shared}" -eq 1 ]]; then run_redacted k -n "${namespace}" get agent,task,runtimepool -o wide || true run_redacted k -n "${namespace}" get events --sort-by=.metadata.creationTimestamp || true fi @@ -1515,6 +1521,10 @@ apply_agent() { }' | k -n "${namespace}" apply -f - >/dev/null } +# Read tasks without Bash carry the restricted {Read,Glob,Grep} tool policy for +# every provider, so codex exercises its native read-only agent mode live. +# Codex cannot express restricted policies that include Bash, so the blocking +# timeout/cancel tasks stay unrestricted for it. apply_read_task() { local name="$1" local agent="$2" @@ -1549,10 +1559,11 @@ apply_read_task() { prompt:$prompt, workspace:({intent:"read",gitRepo:$repo,ref:$ref} + (if ($identity|length)>0 then {sourceRepository:{provider:"github",id:$identity}} else {} end)), - agentRuntime:({maxTurns:12} + (if $provider == "codex" then {} else { - allowBash:$allowBash, - allowedTools:(if $allowBash then ["Read","Glob","Grep","Bash"] else ["Read","Glob","Grep"] end) - } end)), + agentRuntime:({maxTurns:12} + ( + if $provider == "codex" and $allowBash then {} else { + allowBash:$allowBash, + allowedTools:(if $allowBash then ["Read","Glob","Grep","Bash"] else ["Read","Glob","Grep"] end) + } end)), timeout:$timeout } + (if ($session|length)>0 then {sessionRef:{name:$session,create:$create,append:true}} else {} end)) }' | k -n "${namespace}" apply -f - >/dev/null @@ -3213,7 +3224,7 @@ no_cleanup_pull_request_exists() { settle_write_task_for_remote_cleanup() { local pool outcome head number uid [[ "${write_task_started}" -eq 1 ]] || return 0 - [[ "${namespace_created}" -eq 1 && -n "${write_task_name}" ]] || return 1 + [[ ( "${namespace_created}" -eq 1 || "${namespace_shared}" -eq 1 ) && -n "${write_task_name}" ]] || return 1 probe_namespace "${namespace}" || return 1 [[ "${namespace_probe_state}" == "present" ]] || return 1 probe_task "${write_task_name}" || return 1 @@ -3349,22 +3360,35 @@ if [[ "${release_gate}" -eq 1 ]]; then fi if resource_exists get namespace "${namespace}"; then - die "test namespace ${namespace} already exists; choose another --namespace" -fi -namespace_create_attempted=1 -jq -n --arg name "${namespace}" --arg run "${run_id}" '{ - apiVersion:"v1", - kind:"Namespace", - metadata:{ - name:$name, - labels:{ - "orka.ai/acp-e2e-run":$run, - "app.kubernetes.io/managed-by":"live-acp-runtime-e2e", - "pod-security.kubernetes.io/enforce":"restricted" + if k get namespace "${namespace}" -o json | jq -e ' + .metadata.labels["orka.ai/controller-mode"] == "harness-v2" + ' >/dev/null; then + # Shared watch-namespace mode: the isolated harness-v2 controller only + # serves Tasks in its watch namespace, so the validator adopts that + # namespace and cleans up its run-labeled resources without ever owning + # the namespace lifecycle. + log "Adopting existing harness-v2 namespace ${namespace} (shared watch-namespace mode)" + namespace_shared=1 + else + die "test namespace ${namespace} already exists; choose another --namespace" + fi +else + namespace_create_attempted=1 + jq -n --arg name "${namespace}" --arg run "${run_id}" '{ + apiVersion:"v1", + kind:"Namespace", + metadata:{ + name:$name, + labels:{ + "orka.ai/acp-e2e-run":$run, + "orka.ai/controller-mode":"harness-v2", + "app.kubernetes.io/managed-by":"live-acp-runtime-e2e", + "pod-security.kubernetes.io/enforce":"restricted" + } } - } -}' | k create -f - >/dev/null -namespace_created=1 + }' | k create -f - >/dev/null + namespace_created=1 +fi namespace_uid="$(k get namespace "${namespace}" -o jsonpath='{.metadata.uid}')" [[ -n "${namespace_uid}" ]] || die "test namespace UID is unavailable" diff --git a/scripts/live-acp-runtime-kind-e2e.sh b/scripts/live-acp-runtime-kind-e2e.sh index bca525bea..754f41a51 100755 --- a/scripts/live-acp-runtime-kind-e2e.sh +++ b/scripts/live-acp-runtime-kind-e2e.sh @@ -19,6 +19,8 @@ Environment: ACP_E2E_KIND_CONFIG optional Kind config path ACP_E2E_KEEP_CLUSTER=1 keep the cluster and local registry after the run ACP_E2E_VEKIL_IMAGE digest-pinned Vekil image override + ACP_E2E_VEKIL_LOCAL_IMAGE local Docker image published through the run's + registry and digest-pinned there (development builds) ACP_E2E_OPENCODE_MODEL reviewed OpenCode provider/model identifier ACP_E2E_OPENCODE_CONTEXT_WINDOW reviewed OpenCode context capacity (required) ACP_E2E_OPENCODE_MAX_TOKENS reviewed OpenCode output limit (required) @@ -72,7 +74,9 @@ LIVE_ACP_VALIDATOR_SCRIPT="${ACP_E2E_VALIDATOR_SCRIPT:-${repo_root}/scripts/live LIVE_ACP_KIND_TAG="${ACP_E2E_KIND_TAG:-live-acp-runtime-${image_tag}}" LIVE_ACP_KIND_CONFIG="${ACP_E2E_KIND_CONFIG:-}" LIVE_ACP_KEEP_CLUSTER="${ACP_E2E_KEEP_CLUSTER:-0}" -LIVE_ACP_VEKIL_IMAGE="${ACP_E2E_VEKIL_IMAGE:-ghcr.io/sozercan/vekil:v0.14.0@sha256:9e6ab58b9c27888db34d76422c3520b3bf103742a058572439a1fe0aa35a2ade}" +LIVE_ACP_VEKIL_IMAGE="${ACP_E2E_VEKIL_IMAGE:-ghcr.io/sozercan/vekil:v0.14.1@sha256:2fa0558f6304cc6ed1fb5b0135f62f12f28f1cdd0a8c057c4283414bceac1362}" +LIVE_ACP_VEKIL_LOCAL_IMAGE="${ACP_E2E_VEKIL_LOCAL_IMAGE:-}" +export LIVE_ACP_VEKIL_LOCAL_IMAGE LIVE_ACP_ROLLOUT_TIMEOUT="${ACP_E2E_ROLLOUT_TIMEOUT:-10m}" LIVE_ACP_CONTROLLER_IMAGE="orka-controller:live-acp-${image_tag}" LIVE_ACP_CODEX_IMAGE="orka-acp-codex:live-acp-${image_tag}" @@ -141,8 +145,11 @@ trap 'status=$?; live_acp_kind_cleanup "${status}"; exit "${status}"' EXIT live_acp_kind_bootstrap validator_args=(--context "${LIVE_ACP_CONTEXT}") -if [[ -n "${namespace}" ]]; then - validator_args+=(--namespace "${namespace}") +if [[ -z "${namespace}" ]]; then + # The isolated harness-v2 controller only serves Tasks in its watch + # namespace, so the validator runs there in shared watch-namespace mode. + namespace="orka-system" fi +validator_args+=(--namespace "${namespace}") live_acp_kind_log "Running canonical live ACP runtime validator" "${LIVE_ACP_VALIDATOR_SCRIPT}" "${validator_args[@]}" diff --git a/scripts/live-copilot-proxy-e2e.sh b/scripts/live-copilot-proxy-e2e.sh index fb01863bb..b6e8df522 100755 --- a/scripts/live-copilot-proxy-e2e.sh +++ b/scripts/live-copilot-proxy-e2e.sh @@ -30,7 +30,7 @@ copilot_proxy_service_port="${COPILOT_PROXY_SERVICE_PORT:-1337}" provider_proxy_namespace="${ACP_PROVIDER_PROXY_NAMESPACE:-vekil-system}" provider_proxy_service="${ACP_PROVIDER_PROXY_SERVICE:-vekil}" provider_proxy_service_port="${ACP_PROVIDER_PROXY_SERVICE_PORT:-1337}" -copilot_proxy_image="${COPILOT_PROXY_IMAGE:-ghcr.io/sozercan/vekil:v0.14.0@sha256:9e6ab58b9c27888db34d76422c3520b3bf103742a058572439a1fe0aa35a2ade}" +copilot_proxy_image="${COPILOT_PROXY_IMAGE:-ghcr.io/sozercan/vekil:v0.14.1@sha256:2fa0558f6304cc6ed1fb5b0135f62f12f28f1cdd0a8c057c4283414bceac1362}" proxy_token_secret_name="${COPILOT_PROXY_TOKEN_SECRET_NAME:-live-copilot-proxy-token}" token_value="${COPILOT_GITHUB_TOKEN:-}" [[ "${provider_proxy_namespace}" == "vekil-system" && "${provider_proxy_service}" == "vekil" && "${provider_proxy_service_port}" == "1337" ]] || diff --git a/scripts/tests/acp-runtime-image-rollout-test.sh b/scripts/tests/acp-runtime-image-rollout-test.sh index 184567274..9733a8bc8 100755 --- a/scripts/tests/acp-runtime-image-rollout-test.sh +++ b/scripts/tests/acp-runtime-image-rollout-test.sh @@ -931,6 +931,7 @@ assert_converged() { local dependency_first_rollout_line dependency_last_rollout_line local dependency_first_endpoint_line dependency_last_endpoint_line local controller_line controller_rollout_line reference dependency + local converged_start converged_log reference="$(cat "${state_dir}/deployment-ref")" [[ -e "${state_dir}/namespace" ]] [[ -e "${state_dir}/configmaps/${reference}" ]] @@ -950,17 +951,25 @@ assert_converged() { [[ "$(grep -c '^secret:agent-execution-snapshot-key$' "${state_dir}/apply.log")" == "1" ]] [[ "$(grep '^smoke:' "${state_dir}/apply.log" | sort -u | wc -l | tr -d '[:space:]')" == "7" ]] [[ "$(grep -c '^webhooks:orka-admission$' "${state_dir}/apply.log")" -ge 1 ]] - admission_line="$(grep -n '^admission-runtime:orka-admission$' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - admission_rollout_line="$(grep -n '^rollout:orka-admission$' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - smoke_line="$(grep -n '^smoke:' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - webhooks_line="$(grep -n '^webhooks:orka-admission$' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - dependency_line="$(grep -n '^dependencies:' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - dependency_first_rollout_line="$(grep -nE '^rollout:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - dependency_last_rollout_line="$(grep -nE '^rollout:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${state_dir}/apply.log" | tail -1 | cut -d: -f1)" - dependency_first_endpoint_line="$(grep -nE '^endpoint:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - dependency_last_endpoint_line="$(grep -nE '^endpoint:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${state_dir}/apply.log" | tail -1 | cut -d: -f1)" - controller_line="$(grep -n '^full:' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" - controller_rollout_line="$(grep -n '^rollout:orka-controller-manager$' "${state_dir}/apply.log" | head -1 | cut -d: -f1)" + # Recovery scenarios run the apply script twice into one shared log, so + # phase ordering is asserted on the final converged invocation, which always + # starts at its namespace claim. The aborted invocation's partial ordering is + # enforced by the fake kubectl state guards instead. + converged_start="$(grep -nE '^(namespace|namespace-metadata):' "${state_dir}/apply.log" | tail -1 | cut -d: -f1)" + [[ -n "${converged_start}" ]] + converged_log="${state_dir}/apply-converged.log" + tail -n "+${converged_start}" "${state_dir}/apply.log" >"${converged_log}" + admission_line="$(grep -n '^admission-runtime:orka-admission$' "${converged_log}" | head -1 | cut -d: -f1)" + admission_rollout_line="$(grep -n '^rollout:orka-admission$' "${converged_log}" | head -1 | cut -d: -f1)" + smoke_line="$(grep -n '^smoke:' "${converged_log}" | head -1 | cut -d: -f1)" + webhooks_line="$(grep -n '^webhooks:orka-admission$' "${converged_log}" | head -1 | cut -d: -f1)" + dependency_line="$(grep -n '^dependencies:' "${converged_log}" | head -1 | cut -d: -f1)" + dependency_first_rollout_line="$(grep -nE '^rollout:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${converged_log}" | head -1 | cut -d: -f1)" + dependency_last_rollout_line="$(grep -nE '^rollout:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${converged_log}" | tail -1 | cut -d: -f1)" + dependency_first_endpoint_line="$(grep -nE '^endpoint:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${converged_log}" | head -1 | cut -d: -f1)" + dependency_last_endpoint_line="$(grep -nE '^endpoint:(orka-provider-auth-proxy|orka-scm-egress-proxy|orka-workspace-publisher)$' "${converged_log}" | tail -1 | cut -d: -f1)" + controller_line="$(grep -n '^full:' "${converged_log}" | head -1 | cut -d: -f1)" + controller_rollout_line="$(grep -n '^rollout:orka-controller-manager$' "${converged_log}" | head -1 | cut -d: -f1)" (( admission_line < admission_rollout_line )) (( admission_rollout_line < smoke_line )) (( smoke_line < webhooks_line )) diff --git a/test/e2e/cli_security_monitor_test.go b/test/e2e/cli_security_monitor_test.go index da1e041bd..9f2758ba8 100644 --- a/test/e2e/cli_security_monitor_test.go +++ b/test/e2e/cli_security_monitor_test.go @@ -78,9 +78,7 @@ spec: contractVersion: orka.harness.v2 type: claude defaultMaxTurns: 1 - secretRef: - name: %s -`, agentName, secretName)) +`, agentName)) expectOrkaSuccess(runOrka(home, "agent", "create", "-f", agentManifest), token, fakeAnthropicKey) By("creating and reading a RepositoryScan with required repoURL and analysisAgentRef fields") diff --git a/website/docs/guides/repository-monitors.md b/website/docs/guides/repository-monitors.md index 8d5c4625a..8f9514bb0 100644 --- a/website/docs/guides/repository-monitors.md +++ b/website/docs/guides/repository-monitors.md @@ -37,7 +37,7 @@ The first implementation is intentionally narrow: - `spec.review.requireGreenCI` gates review selection until CI is green. - GitHub webhook-driven exact runs are opt-in with `spec.review.exactEventEnabled`. - Repair, maintainer command routing, issue action workflows, implementation budgets (`maxActive`, `maxAttemptsPerIssue`, `maxChangedFiles`, `allowedPaths`), and optional head-bound automerge are active monitor-owned workflows. Automerge remains disabled by default and requires explicit configuration plus a one-shot command. -- Built-in reviewer Agents may use `runtime.type: claude`, `codex`, or `opencode`. They must omit `spec.secretRef`; provider credentials come from the controller-managed runtime proxy and never enter the Task spec. +- Built-in reviewer Agents may use `runtime.type: claude`, `codex`, or `opencode`. Codex reviewers are confined by the RuntimeSession boundary: elevation requests are rejected by the controller, file writes are mediated by the supervisor, and the read-intent workspace delta classification fails any turn that modifies the workspace. Reviewer Agents must omit `spec.secretRef`; provider credentials come from the controller-managed runtime proxy and never enter the Task spec. ## CI Coverage diff --git a/workers/acp/supervisor/env.go b/workers/acp/supervisor/env.go index 4c29833e5..9d93e4915 100644 --- a/workers/acp/supervisor/env.go +++ b/workers/acp/supervisor/env.go @@ -283,6 +283,10 @@ func providerAdapterDigests(provider string) map[string]string { } } +// codexAgentModeOrkaExternal is the Orka-patched codex-acp agent mode whose +// externalSandbox policy keeps the runtime Pod as the enforcement boundary. +const codexAgentModeOrkaExternal = "orka-external" + var providerNativeToolNames = []string{ providerToolBash, providerToolEdit, providerToolGlob, providerToolGrep, providerToolRead, providerToolWebFetch, providerToolWebSearch, providerToolWrite, @@ -367,7 +371,7 @@ func codexSessionProjection( if err != nil { return ProviderSessionProjection{}, err } - if !policy.unrestricted { + if !policy.unrestricted && !codexReadOnlySessionPolicy(request, policy) { return ProviderSessionProjection{}, fmt.Errorf("codex ACP runtime cannot exactly enforce provider-native tool restrictions") } config := map[string]any{ @@ -387,9 +391,29 @@ func codexSessionProjection( if len(encoded) > maxCodexConfigEnvironmentBytes { return ProviderSessionProjection{}, fmt.Errorf("codex session configuration exceeds the safe environment limit") } + // Read-only sessions keep the default orka-external agent mode: Codex's + // own sandbox needs unprivileged user namespaces that the runtime Pod + // forbids, so the RuntimeSession boundary enforces the surface instead — + // safe read commands execute, every elevation request is rejected + // unconditionally by the controller, file writes are mediated by the + // supervisor, and the read-intent workspace delta classification fails + // any turn that modifies the workspace. return ProviderSessionProjection{Environment: map[string]string{"CODEX_CONFIG": string(encoded)}}, nil } +// codexReadOnlySessionPolicy reports whether the session's restricted tool +// policy is exactly the read-intent {Glob, Grep, Read} surface, which is the +// only restricted shape the codex read-only agent mode enforces. +func codexReadOnlySessionPolicy(request harnessv2.CreateRuntimeSessionRequest, policy providerNativePolicy) bool { + if request.Profile.WorkspaceIntent != harnessv2.WorkspaceIntentRead { + return false + } + if len(policy.allowed) != 3 { + return false + } + return policy.allows(providerToolGlob) && policy.allows(providerToolGrep) && policy.allows(providerToolRead) +} + func claudeSessionProjection( request harnessv2.CreateRuntimeSessionRequest, _ acp.SessionPaths, @@ -532,7 +556,7 @@ func providerProfile( // restricted Runtime Pod remains the enforcement boundary without asking // the child to create nested Linux namespaces. Network remains restricted // and on-request approvals remain active for explicit elevation requests. - mode := "orka-external" + mode := codexAgentModeOrkaExternal config, err := json.Marshal(map[string]any{ "model": model, "openai_base_url": proxy.BaseURL, "check_for_update_on_startup": false, }) diff --git a/workers/acp/supervisor/env_test.go b/workers/acp/supervisor/env_test.go index 57299c647..4af9522f0 100644 --- a/workers/acp/supervisor/env_test.go +++ b/workers/acp/supervisor/env_test.go @@ -120,6 +120,46 @@ func TestCodexProviderSessionProjection(t *testing.T) { } } +func TestCodexProviderSessionProjectionReadOnlySurface(t *testing.T) { + paths := acp.SessionPaths{Home: "/sessions/private/home"} + proxy := ProviderProxyBinding{BaseURL: "http://127.0.0.1:43210/_orka/provider/session", Credential: "test-auth-token"} + codex, err := providerProfile(providerKindCodex, "gpt-test", harnessv2.WorkspaceIntentRead) + if err != nil { + t.Fatal(err) + } + surface := []string{providerToolGlob, providerToolGrep, providerToolRead} + request := testProviderProjectionRequest(t, providerKindCodex, "gpt-test", "", "", surface, nil, false) + projection, err := codex.ProjectSession(request, paths, proxy) + if err != nil { + t.Fatalf("read-only codex projection error = %v", err) + } + environment, err := codex.EnvironmentForSession(request, paths, proxy) + if err != nil { + t.Fatal(err) + } + maps.Copy(environment, projection.Environment) + // Read-only sessions keep the orka-external agent mode: Codex's own + // sandbox needs unprivileged user namespaces the runtime Pod forbids, so + // the RuntimeSession boundary enforces the read-only surface instead. + if environment["INITIAL_AGENT_MODE"] != codexAgentModeOrkaExternal { + t.Fatalf("INITIAL_AGENT_MODE = %q, want orka-external", environment["INITIAL_AGENT_MODE"]) + } + if !strings.Contains(environment["CODEX_CONFIG"], proxy.BaseURL) || environment["CODEX_API_KEY"] != proxy.Credential { + t.Fatalf("unexpected Codex environment: %#v", environment) + } + + rejected := testProviderProjectionRequest(t, providerKindCodex, "gpt-test", "", "", []string{providerToolGlob, providerToolRead, providerToolWrite}, nil, false) + if _, err := codex.ProjectSession(rejected, paths, proxy); err == nil { + t.Fatal("restricted codex projection with Write was accepted") + } + + writeIntent := testProviderProjectionRequest(t, providerKindCodex, "gpt-test", "", "", surface, nil, false) + writeIntent.Profile.WorkspaceIntent = harnessv2.WorkspaceIntentWrite + if _, err := codex.ProjectSession(writeIntent, paths, proxy); err == nil { + t.Fatal("write-intent restricted codex projection was accepted") + } +} + func TestClaudeProviderSessionProjection(t *testing.T) { paths := acp.SessionPaths{Home: "/sessions/private/home"} proxy := ProviderProxyBinding{BaseURL: "http://127.0.0.1:43210/_orka/provider/session", Credential: "test-auth-token"} @@ -318,7 +358,7 @@ func TestCodexProviderProfileUsesExternalRuntimeSandbox(t *testing.T) { if err != nil { t.Fatal(err) } - if got := environment["INITIAL_AGENT_MODE"]; got != "orka-external" { + if got := environment["INITIAL_AGENT_MODE"]; got != codexAgentModeOrkaExternal { t.Fatalf("INITIAL_AGENT_MODE = %q, want orka-external", got) } }) diff --git a/workers/acp/supervisor/prompt_handlers.go b/workers/acp/supervisor/prompt_handlers.go index e98c327d5..4c0d037ec 100644 --- a/workers/acp/supervisor/prompt_handlers.go +++ b/workers/acp/supervisor/prompt_handlers.go @@ -1174,7 +1174,7 @@ func workspaceDeltaPathAllowed(changedPath string, patterns []string) bool { if strings.HasSuffix(patternValue, "/**") && strings.HasPrefix(changedPath, strings.TrimSuffix(patternValue, "**")) { return true } - if matched, err := path.Match(patternValue, changedPath); err == nil && matched { + if workspaceDeltaPatternMatches(patternValue, changedPath) { return true } if strings.TrimSuffix(patternValue, "/") == strings.TrimSuffix(changedPath, "/") { @@ -1184,6 +1184,51 @@ func workspaceDeltaPathAllowed(changedPath string, patterns []string) bool { return false } +// workspaceDeltaPatternMatches applies gitignore-style glob semantics: a `**` +// segment matches zero or more whole path segments, while every other segment +// keeps path.Match single-segment semantics. +func workspaceDeltaPatternMatches(patternValue, changedPath string) bool { + if !strings.Contains(patternValue, "**") { + matched, err := path.Match(patternValue, changedPath) + return err == nil && matched + } + return workspaceDeltaSegmentsMatch(strings.Split(patternValue, "/"), strings.Split(changedPath, "/")) +} + +// workspaceDeltaSegmentsMatch uses the classic greedy wildcard algorithm at +// segment granularity — backtracking only to the most recent `**` — so +// matching stays O(pattern × path) even for agent-controlled paths against +// patterns with many `**` segments. +func workspaceDeltaSegmentsMatch(patternSegments, pathSegments []string) bool { + patternIndex, pathIndex := 0, 0 + starPattern, starPath := -1, 0 + for pathIndex < len(pathSegments) { + switch { + case patternIndex < len(patternSegments) && patternSegments[patternIndex] == "**": + starPattern, starPath = patternIndex, pathIndex + patternIndex++ + case patternIndex < len(patternSegments) && workspaceDeltaSegmentMatches(patternSegments[patternIndex], pathSegments[pathIndex]): + patternIndex++ + pathIndex++ + case starPattern >= 0: + starPath++ + pathIndex = starPath + patternIndex = starPattern + 1 + default: + return false + } + } + for patternIndex < len(patternSegments) && patternSegments[patternIndex] == "**" { + patternIndex++ + } + return patternIndex == len(patternSegments) +} + +func workspaceDeltaSegmentMatches(pattern, segment string) bool { + matched, err := path.Match(pattern, segment) + return err == nil && matched +} + func workspaceDeltaRepositoryControlPathForWorkspace(workspaceRelativeRoot, changedPath string) bool { return workspaceDeltaRepositoryControlPath(path.Join(strings.TrimSpace(workspaceRelativeRoot), changedPath)) } diff --git a/workers/acp/supervisor/prompt_handlers_test.go b/workers/acp/supervisor/prompt_handlers_test.go index be1f31dc3..496275704 100644 --- a/workers/acp/supervisor/prompt_handlers_test.go +++ b/workers/acp/supervisor/prompt_handlers_test.go @@ -853,6 +853,50 @@ func TestWorkspaceDeltaPathPolicy(t *testing.T) { } } +func TestWorkspaceDeltaPathAllowedRecursiveGlobs(t *testing.T) { + allowed := []struct { + path string + patterns []string + }{ + // `**` must span multiple directory levels, gitignore-style. + {path: "src/pkg/util/helpers.go", patterns: []string{"src/**/*.go"}}, + {path: "src/one.go", patterns: []string{"src/**/*.go"}}, + {path: "main.go", patterns: []string{"**/*.go"}}, + {path: "a/b/c/d.txt", patterns: []string{"a/**/d.txt"}}, + {path: "a/d.txt", patterns: []string{"a/**/d.txt"}}, + {path: "internal/deep/nested/file.go", patterns: []string{"internal/**"}}, + {path: "docs/guide/intro.md", patterns: []string{"docs/*/intro.md"}}, + } + for _, tt := range allowed { + if !workspaceDeltaPathAllowed(tt.path, tt.patterns) { + t.Errorf("expected %q to match %v", tt.path, tt.patterns) + } + } + denied := []struct { + path string + patterns []string + }{ + {path: "src/pkg/util/helpers.txt", patterns: []string{"src/**/*.go"}}, + {path: "other/one.go", patterns: []string{"src/**/*.go"}}, + {path: "docs/guide/deep/intro.md", patterns: []string{"docs/*/intro.md"}}, + {path: "srcfile.go", patterns: []string{"src/**"}}, + } + for _, tt := range denied { + if workspaceDeltaPathAllowed(tt.path, tt.patterns) { + t.Errorf("expected %q to not match %v", tt.path, tt.patterns) + } + } + // Multiple ** segments over a long non-matching path must complete without + // backtracking blowup. + hostile := strings.Repeat("a/", 512) + "z.txt" + if workspaceDeltaPathAllowed(hostile, []string{"**/b/**/c/**/d/**/e/**/*.go"}) { + t.Fatal("hostile path unexpectedly matched") + } + if !workspaceDeltaPathAllowed("a/x/b/y/c/z/d/w/e/final.go", []string{"a/**/b/**/c/**/d/**/e/**/*.go"}) { + t.Fatal("interleaved multi-star pattern failed to match") + } +} + func TestWorkspaceDeltaRejectsSessionCredentials(t *testing.T) { state := &sessionState{ providerProxy: &providerProxySession{credential: []byte("provider-session-secret")}, diff --git a/workers/acp/supervisor/provider_proxy.go b/workers/acp/supervisor/provider_proxy.go index d86ad8fc1..1d23966e2 100644 --- a/workers/acp/supervisor/provider_proxy.go +++ b/workers/acp/supervisor/provider_proxy.go @@ -19,6 +19,8 @@ import ( "strings" "sync" "time" + + "github.com/orka-agents/orka/internal/providerproxy" ) const ( @@ -161,7 +163,7 @@ func (c ProviderProxyConfig) normalized() (ProviderProxyConfig, *url.URL, error) if c.UpstreamBearerToken == "" || strings.IndexFunc(c.UpstreamBearerToken, func(value rune) bool { return value <= ' ' || value == 0x7f }) >= 0 { return ProviderProxyConfig{}, nil, fmt.Errorf("provider proxy upstream bearer token is required") } - if hasUnsafePathSegment(parsed.Path) { + if providerproxy.HasUnsafePathSegment(parsed.Path) { return ProviderProxyConfig{}, nil, fmt.Errorf("provider proxy upstream URL is invalid") } if c.MaxRequestBytes <= 0 { @@ -550,30 +552,30 @@ func (p *providerProxy) serveHTTP(w http.ResponseWriter, r *http.Request) { } authorization, ok := session.authorize(r, time.Now().UTC()) if !ok { - writeProviderProxyError(w, http.StatusForbidden, "provider access is not active") + providerproxy.WriteError(w, http.StatusForbidden, "provider access is not active") return } defer authorization.release() - if !tryAcquireProviderSlot(session.requestSlots) { - writeProviderProxyError(w, http.StatusTooManyRequests, "provider session request capacity is exhausted") + if !providerproxy.TryAcquireSlot(session.requestSlots) { + providerproxy.WriteError(w, http.StatusTooManyRequests, "provider session request capacity is exhausted") return } - defer releaseProviderSlot(session.requestSlots) - if !tryAcquireProviderSlot(p.requestSlots) { - writeProviderProxyError(w, http.StatusTooManyRequests, "provider proxy request capacity is exhausted") + defer providerproxy.ReleaseSlot(session.requestSlots) + if !providerproxy.TryAcquireSlot(p.requestSlots) { + providerproxy.WriteError(w, http.StatusTooManyRequests, "provider proxy request capacity is exhausted") return } - defer releaseProviderSlot(p.requestSlots) + defer providerproxy.ReleaseSlot(p.requestSlots) if r.Method == http.MethodConnect || r.Method == http.MethodTrace { - writeProviderProxyError(w, http.StatusMethodNotAllowed, "provider request method is not allowed") + providerproxy.WriteError(w, http.StatusMethodNotAllowed, "provider request method is not allowed") return } - if hasUnsafePathSegment(suffix) { - writeProviderProxyError(w, http.StatusBadRequest, "provider request path is invalid") + if providerproxy.HasUnsafePathSegment(suffix) { + providerproxy.WriteError(w, http.StatusBadRequest, "provider request path is invalid") return } - if encoding := strings.TrimSpace(r.Header.Get(providerContentEncodingHeader)); encoding != "" && !strings.EqualFold(encoding, "identity") { - writeProviderProxyError(w, http.StatusUnsupportedMediaType, "compressed provider requests are forbidden") + if providerproxy.HasDisallowedContentEncoding(r.Header) { + providerproxy.WriteError(w, http.StatusUnsupportedMediaType, "compressed provider requests are forbidden") return } @@ -601,25 +603,25 @@ func (p *providerProxy) serveHTTP(w http.ResponseWriter, r *http.Request) { body, err := readBoundedProviderBody(requestContext, r.Body, p.maxRequestBytes) if err != nil { if errors.Is(err, errProviderBodyTooLarge) { - writeProviderProxyError(w, http.StatusRequestEntityTooLarge, "provider request body exceeds limit") + providerproxy.WriteError(w, http.StatusRequestEntityTooLarge, "provider request body exceeds limit") } else { - writeProviderProxyError(w, http.StatusForbidden, "provider request is no longer active") + providerproxy.WriteError(w, http.StatusForbidden, "provider request is no longer active") } return } requestClass, err := validateProviderRequest(p.providerKind, p.model, suffix, r.Method, body) if err != nil { - writeProviderProxyError(w, http.StatusForbidden, "provider request is outside the immutable profile") + providerproxy.WriteError(w, http.StatusForbidden, "provider request is outside the immutable profile") return } body, err = normalizeProviderRequestBody(p.providerKind, p.model, suffix, p.modelOutputLimit, body) if err != nil { - writeProviderProxyError(w, http.StatusForbidden, "provider request is outside the immutable profile") + providerproxy.WriteError(w, http.StatusForbidden, "provider request is outside the immutable profile") return } select { case <-authorization.gateContext.Done(): - writeProviderProxyError(w, http.StatusForbidden, "provider request is no longer active") + providerproxy.WriteError(w, http.StatusForbidden, "provider request is no longer active") return default: } @@ -627,7 +629,7 @@ func (p *providerProxy) serveHTTP(w http.ResponseWriter, r *http.Request) { if errors.Is(err, errProviderTurnLimitExceeded) { writeProviderTurnLimitError(w, p.providerKind) } else { - writeProviderProxyError(w, http.StatusForbidden, "provider request is no longer active") + providerproxy.WriteError(w, http.StatusForbidden, "provider request is no longer active") } return } @@ -640,37 +642,40 @@ func (p *providerProxy) serveHTTP(w http.ResponseWriter, r *http.Request) { } connectionMu.Unlock() }}) - target := providerProxyTarget(authorization.upstreamBase, suffix, r.URL.RawQuery) + target := providerproxy.Target(authorization.upstreamBase, suffix, r.URL.RawQuery) upstreamRequest, err := http.NewRequestWithContext(requestContext, r.Method, target.String(), bytes.NewReader(body)) if err != nil { - writeProviderProxyError(w, http.StatusBadGateway, "provider request could not be prepared") + providerproxy.WriteError(w, http.StatusBadGateway, "provider request could not be prepared") return } - copyProviderRequestHeaders(upstreamRequest.Header, r.Header) + providerproxy.CopyRequestHeaders(upstreamRequest.Header, r.Header) upstreamRequest.Header.Set(providerAuthorizationHeader, "Bearer "+string(p.upstreamToken)) upstreamRequest.Header.Set("Accept-Encoding", "identity") response, err := p.client.Do(upstreamRequest) if err != nil { - writeProviderProxyError(w, http.StatusBadGateway, "provider upstream request failed") + providerproxy.WriteError(w, http.StatusBadGateway, "provider upstream request failed") return } defer response.Body.Close() //nolint:errcheck if response.StatusCode >= http.StatusMultipleChoices && response.StatusCode < http.StatusBadRequest { - writeProviderProxyError(w, http.StatusBadGateway, "provider upstream redirects are forbidden") + providerproxy.WriteError(w, http.StatusBadGateway, "provider upstream redirects are forbidden") return } - if encoding := strings.TrimSpace(response.Header.Get(providerContentEncodingHeader)); encoding != "" && !strings.EqualFold(encoding, "identity") { - writeProviderProxyError(w, http.StatusBadGateway, "compressed provider responses are forbidden") + if providerproxy.HasDisallowedContentEncoding(response.Header) { + providerproxy.WriteError(w, http.StatusBadGateway, "compressed provider responses are forbidden") return } if response.ContentLength > p.maxResponseBytes { - writeProviderProxyError(w, http.StatusBadGateway, "provider upstream response exceeds limit") + providerproxy.WriteError(w, http.StatusBadGateway, "provider upstream response exceeds limit") return } - copyProviderResponseHeaders(w.Header(), response.Header) + providerproxy.CopyResponseHeaders(w.Header(), response.Header) w.WriteHeader(response.StatusCode) - if err := streamBoundedProviderResponse(w, response.Body, p.maxResponseBytes); err != nil { + // Flushing after every chunk keeps streamed provider responses (SSE) + // flowing to the ACP child without buffering delays. + flusher, _ := w.(http.Flusher) + if err := providerproxy.StreamBoundedResponse(w, response.Body, p.maxResponseBytes, flusher); err != nil { panic(http.ErrAbortHandler) } } @@ -820,33 +825,6 @@ func readBoundedProviderBody(ctx context.Context, body io.ReadCloser, limit int6 return data, nil } -func streamBoundedProviderResponse(w http.ResponseWriter, body io.Reader, limit int64) error { - buffer := make([]byte, 32<<10) - var written int64 - flusher, _ := w.(http.Flusher) - for { - n, err := body.Read(buffer) - if n > 0 { - if written+int64(n) > limit { - return errProviderBodyTooLarge - } - if _, writeErr := w.Write(buffer[:n]); writeErr != nil { - return writeErr - } - written += int64(n) - if flusher != nil { - flusher.Flush() - } - } - if err != nil { - if errors.Is(err, io.EOF) { - return nil - } - return err - } - } -} - func splitProviderProxyRoute(path string) (route string, ok bool) { if !strings.HasPrefix(path, providerProxyPathPrefix) { return "", false @@ -859,17 +837,6 @@ func splitProviderProxyRoute(path string) (route string, ok bool) { return route, true } -func tryAcquireProviderSlot(slots chan struct{}) bool { - select { - case slots <- struct{}{}: - return true - default: - return false - } -} - -func releaseProviderSlot(slots chan struct{}) { <-slots } - func (s *providerProxySession) requestSuffix(path string) (string, bool) { if path == s.basePath { return "/", true @@ -880,106 +847,6 @@ func (s *providerProxySession) requestSuffix(path string) (string, bool) { return strings.TrimPrefix(path, s.basePath), true } -func hasUnsafePathSegment(path string) bool { - if strings.Contains(path, "\\") || strings.ContainsRune(path, '\x00') { - return true - } - for segment := range strings.SplitSeq(path, "/") { - if segment == "." || segment == ".." { - return true - } - } - return false -} - -func providerProxyTarget(base *url.URL, suffix, rawQuery string) *url.URL { - target := *base - basePath := strings.TrimSuffix(target.Path, "/") - if suffix == "/" { - target.Path = basePath + "/" - } else { - target.Path = basePath + suffix - } - target.RawPath = "" - target.RawQuery = rawQuery - return &target -} - -func copyProviderRequestHeaders(destination, source http.Header) { - blocked := providerBlockedHeaders(source) - for name, values := range source { - canonical := http.CanonicalHeaderKey(name) - if blocked[canonical] || isSensitiveProviderRequestHeader(canonical) { - continue - } - for _, value := range values { - destination.Add(canonical, value) - } - } -} - -func copyProviderResponseHeaders(destination, source http.Header) { - blocked := providerBlockedHeaders(source) - for name, values := range source { - canonical := http.CanonicalHeaderKey(name) - if blocked[canonical] || isSensitiveProviderResponseHeader(canonical) { - continue - } - for _, value := range values { - destination.Add(canonical, value) - } - } -} - -func providerBlockedHeaders(header http.Header) map[string]bool { - blocked := map[string]bool{ - "Connection": true, - "Keep-Alive": true, - "Proxy-Authenticate": true, - providerProxyAuthorizationHeader: 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 isSensitiveProviderRequestHeader(name string) bool { - switch name { - case providerAuthorizationHeader, providerProxyAuthorizationHeader, providerAPIKeyHeader, providerLegacyAPIKeyHeader, - providerCookieHeader, "Set-Cookie", "Forwarded", providerForwardedForHeader, "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 isSensitiveProviderResponseHeader(name string) bool { - switch name { - case providerAuthorizationHeader, providerProxyAuthorizationHeader, providerAPIKeyHeader, providerLegacyAPIKeyHeader, - "Set-Cookie", "Set-Cookie2", "Location", "Server", "Alt-Svc", "Www-Authenticate", - "Proxy-Authenticate", providerContentEncodingHeader: - return true - default: - return false - } -} - func writeProviderTurnLimitError(w http.ResponseWriter, providerKind string) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") @@ -991,13 +858,6 @@ func writeProviderTurnLimitError(w http.ResponseWriter, providerKind string) { _, _ = io.WriteString(w, `{"error":{"message":"maximum provider inference requests reached for active prompt","type":"invalid_request_error","code":"max_turn_requests"}}`+"\n") } -func writeProviderProxyError(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 (p *providerProxy) close(ctx context.Context) error { if p == nil { return nil diff --git a/workers/harness/cliwrapper/result.go b/workers/harness/cliwrapper/result.go index 2c87f8b57..56972dc0c 100644 --- a/workers/harness/cliwrapper/result.go +++ b/workers/harness/cliwrapper/result.go @@ -22,7 +22,12 @@ const wrapperSafeCommandPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi const turnMetadataSkillsFiles = "skillsFiles" -const wrapperGitPostTurnTimeout = 30 * time.Second +// wrapperGitPostTurnTimeout bounds each post-turn git finalization phase +// (ShouldFinalizeWorkDir, FinalizeTurnResult, CleanFinalizedWorkDir) so a hung +// git process cannot wedge the turn. It is applied per phase, never shared +// across phases, and must stay generous enough for `git add -A` plus a cached +// binary diff on multi-gigabyte working trees. +const wrapperGitPostTurnTimeout = 5 * time.Minute var wrapperGitBinary = resolveSafeExecutable("git") diff --git a/workers/harness/cliwrapper/runtime_auth_proxy.go b/workers/harness/cliwrapper/runtime_auth_proxy.go index ea5080796..149676fa7 100644 --- a/workers/harness/cliwrapper/runtime_auth_proxy.go +++ b/workers/harness/cliwrapper/runtime_auth_proxy.go @@ -121,14 +121,19 @@ func protectRuntimeAuthTurn(turn TurnContext) (TurnContext, string, func(), erro }) } localEndpoint := (&url.URL{Scheme: runtimeAuthProxyHTTP, Host: listener.Addr().String(), Path: upstream.Path}).String() + // Runtime-auth-only turns promise the child sees no raw provider + // credential, so scrub every known provider credential — not only the + // proxied provider's — before injecting the per-turn loopback values. + turn.Env = removeTurnEnv(turn.Env, + workerenv.OpenAIBaseURL, workerenv.OpenAIAPIKey, workerenv.CodexAPIKey, + workerenv.AnthropicBaseURL, workerenv.AnthropicAPIKey, "ANTHROPIC_FOUNDRY_API_KEY", + ) switch mode { case runtimeAuthProxyOpenAI: - turn.Env = removeTurnEnv(turn.Env, workerenv.OpenAIBaseURL, workerenv.OpenAIAPIKey, workerenv.CodexAPIKey) turn.Env = setEnv(turn.Env, workerenv.OpenAIBaseURL, localEndpoint) turn.Env = setEnv(turn.Env, workerenv.OpenAIAPIKey, token) turn.Env = setEnv(turn.Env, workerenv.CodexAPIKey, token) case runtimeAuthProxyAnthropic: - turn.Env = removeTurnEnv(turn.Env, workerenv.AnthropicBaseURL, workerenv.AnthropicAPIKey) turn.Env = setEnv(turn.Env, workerenv.AnthropicBaseURL, localEndpoint) turn.Env = setEnv(turn.Env, workerenv.AnthropicAPIKey, token) } diff --git a/workers/harness/cliwrapper/runtime_auth_proxy_test.go b/workers/harness/cliwrapper/runtime_auth_proxy_test.go index be1567d37..891427a04 100644 --- a/workers/harness/cliwrapper/runtime_auth_proxy_test.go +++ b/workers/harness/cliwrapper/runtime_auth_proxy_test.go @@ -22,17 +22,20 @@ func TestProtectRuntimeAuthTurnUsesLoopbackProxy(t *testing.T) { runtimeName string baseField string authField string + crossField string requestHeader string upstreamPath string }{ { name: "codex", runtimeName: RuntimeCodex, baseField: workerenv.OpenAIBaseURL, authField: workerenv.OpenAIAPIKey, + crossField: workerenv.AnthropicAPIKey, requestHeader: "Authorization", upstreamPath: "/v1/responses", }, { name: "claude", runtimeName: RuntimeClaude, baseField: workerenv.AnthropicBaseURL, authField: workerenv.AnthropicAPIKey, + crossField: workerenv.OpenAIAPIKey, requestHeader: "x-api-key", upstreamPath: "/v1/messages", }, } @@ -54,12 +57,14 @@ func TestProtectRuntimeAuthTurnUsesLoopbackProxy(t *testing.T) { _, _ = io.WriteString(w, `{"ok":true}`) })) basePath := strings.TrimSuffix(tt.upstreamPath, strings.TrimPrefix(tt.upstreamPath, "/v1")) + crossProviderValue := "other-provider-credential" turn := TurnContext{ RuntimeName: tt.runtimeName, Metadata: map[string]string{"runtimeAuthOnly": "true"}, Env: []string{ tt.baseField + "=" + upstream.URL + basePath, tt.authField + "=" + upstreamValue, + tt.crossField + "=" + crossProviderValue, "NO_PROXY=existing.internal", "no_proxy=lower.internal", }, @@ -72,6 +77,11 @@ func TestProtectRuntimeAuthTurnUsesLoopbackProxy(t *testing.T) { if strings.Contains(strings.Join(protected.Env, "\n"), upstreamValue) { t.Fatal("protected child environment retained the upstream credential") } + // Runtime-auth-only turns must scrub other providers' credentials + // too, not only the proxied provider's. + if strings.Contains(strings.Join(protected.Env, "\n"), crossProviderValue) { + t.Fatal("protected child environment retained another provider's credential") + } for _, name := range []string{"NO_PROXY", "no_proxy"} { value := envEntryValue(protected.Env, name) for _, want := range []string{"existing.internal", "lower.internal", runtimeAuthProxyLoopback, "localhost"} { diff --git a/workers/harness/cliwrapper/server.go b/workers/harness/cliwrapper/server.go index 8c80f951a..18bc8d823 100644 --- a/workers/harness/cliwrapper/server.go +++ b/workers/harness/cliwrapper/server.go @@ -1306,19 +1306,17 @@ func (s *Server) runTurn(turn *turnState) { //nolint:gocyclo if preparedWorkspace.rootDir != "" { finalizeWorkDir = preparedWorkspace.rootDir } - gitCtx, cancelGit := context.WithTimeout(ctx, wrapperGitPostTurnTimeout) - defer cancelGit() shouldFinalize := false if !envEntryIsTrue(turnCtx.Env, workerenv.ResultStdout) { var shouldFinalizeErr error - shouldFinalize, shouldFinalizeErr = ShouldFinalizeWorkDir(gitCtx, finalizeWorkDir) + shouldFinalize, shouldFinalizeErr = ShouldFinalizeWorkDir(ctx, finalizeWorkDir) if shouldFinalizeErr != nil { turn.appendFrame(s.runtimeLogTextFrame(turn, "result-finalize", shouldFinalizeErr.Error())) } } if shouldFinalize { restoreTurnEnv := setTemporaryEnvEntries(turnCtx.Env) - if finalized, finalizeErr := FinalizeTurnResult(gitCtx, finalizeWorkDir, partial); finalizeErr != nil { + if finalized, finalizeErr := FinalizeTurnResult(ctx, finalizeWorkDir, partial); finalizeErr != nil { turn.appendFrame(s.runtimeLogTextFrame(turn, "result-finalize", finalizeErr.Error())) } else { partial = string(finalized) @@ -1334,7 +1332,7 @@ func (s *Server) runTurn(turn *turnState) { //nolint:gocyclo )) } if finalizedWorkDir != "" { - if cleanErr := CleanFinalizedWorkDir(gitCtx, finalizedWorkDir); cleanErr != nil { + if cleanErr := CleanFinalizedWorkDir(ctx, finalizedWorkDir); cleanErr != nil { turn.appendFrame(s.runtimeLogTextFrame(turn, "workdir-cleanup", cleanErr.Error())) } } @@ -1375,19 +1373,17 @@ func (s *Server) runTurn(turn *turnState) { //nolint:gocyclo if preparedWorkspace.rootDir != "" { finalizeWorkDir = preparedWorkspace.rootDir } - gitCtx, cancelGit := context.WithTimeout(ctx, wrapperGitPostTurnTimeout) - defer cancelGit() shouldFinalize := false if !envEntryIsTrue(turnCtx.Env, workerenv.ResultStdout) { var shouldFinalizeErr error - shouldFinalize, shouldFinalizeErr = ShouldFinalizeWorkDir(gitCtx, finalizeWorkDir) + shouldFinalize, shouldFinalizeErr = ShouldFinalizeWorkDir(ctx, finalizeWorkDir) if shouldFinalizeErr != nil { turn.appendFrame(s.failedFrame(turn, "result_finalize_failed", shouldFinalizeErr.Error(), false)) return } } if shouldFinalize { - finalized, finalizeErr := FinalizeTurnResult(gitCtx, finalizeWorkDir, parsed.Result) + finalized, finalizeErr := FinalizeTurnResult(ctx, finalizeWorkDir, parsed.Result) if finalizeErr != nil { turn.appendFrame(s.failedFrame(turn, "result_finalize_failed", finalizeErr.Error(), false)) return @@ -1427,7 +1423,7 @@ func (s *Server) runTurn(turn *turnState) { //nolint:gocyclo return } if finalizedWorkDir != "" { - if cleanErr := CleanFinalizedWorkDir(gitCtx, finalizedWorkDir); cleanErr != nil { + if cleanErr := CleanFinalizedWorkDir(ctx, finalizedWorkDir); cleanErr != nil { turn.appendFrame(s.runtimeLogTextFrame( turn, "workdir-cleanup",