Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
265ed55
feat(agents): add versioned soul prompts across built-in runtimes
sozercan Sep 15, 2026
5ddf63d
chore(agents): fix soul lint findings
sozercan Sep 15, 2026
d0ee80e
test(agents): accept native OpenCode prompts in CRD validation
sozercan Sep 15, 2026
c29d114
fix(agents): enforce soul lifecycle and runtime constraints
sozercan Sep 15, 2026
a78bc88
fix(agents): pin soul sessions before execution
sozercan Sep 15, 2026
1495c0b
fix(agents): validate default instruction delivery at readiness
sozercan Sep 15, 2026
7d51ed3
fix(agents): reject unsupported soul task routes
sozercan Sep 15, 2026
b15dab5
fix(agents): watch every referenced role ConfigMap
sozercan Sep 16, 2026
d020fa5
fix(agents): validate soul inputs and retry edited AI tasks
sozercan Sep 16, 2026
71aa8d5
fix(agents): pin no-soul session identity before execution
sozercan Sep 16, 2026
4cebf2e
fix(agents): preserve soul identity through cleanup and startup errors
sozercan Sep 16, 2026
770c58f
fix(runtime): preserve exact native boot retirement evidence
sozercan Sep 17, 2026
94ec00b
fix(runtime): settle abandoned validation with fenced cleanup
sozercan Sep 17, 2026
8b3bc31
fix(agents): subordinate soul quirks to task output contracts
sozercan Sep 17, 2026
1ccef29
chore: merge main into soul validation branch
sozercan Sep 17, 2026
fee8def
fix(runtime): verify reordered workspace preparation evidence
sozercan Sep 17, 2026
201e3b2
fix(agents): default runtime contract before soul validation
sozercan Sep 17, 2026
dfac7b7
fix(ui): reject Copilot instruction imports before submission
sozercan Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion api/v1alpha1/agent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (

// AgentSpec defines the desired state of Agent
// +kubebuilder:validation:XValidation:rule="!has(self.execution) || !has(self.execution.workspace) || !has(self.execution.workspace.classRef)",message="execution.workspace.classRef is only supported on Task specs"
// +kubebuilder:validation:XValidation: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)))",message="opencode orka.harness.v2 runtime does not support spec.systemPrompt"
type AgentSpec struct {
// ProviderRef references a Provider CRD for LLM configuration
// If set, model.provider is optional (inherited from Provider)
Expand All @@ -31,6 +30,11 @@ type AgentSpec struct {
// +optional
SystemPrompt *PromptSource `json:"systemPrompt,omitempty"`

// Soul supplies persistent persona and communication defaults, separate from the role prompt.
// Supported by AI workers and built-in harness v2 runtimes, not runtimeRef runtimes.
// +optional
Soul *SoulSource `json:"soul,omitempty"`

// Tools lists the default tools available to this agent
// +optional
Tools []ToolReference `json:"tools,omitempty"`
Expand Down Expand Up @@ -215,6 +219,28 @@ type PromptSource struct {
ConfigMapRef *ConfigMapKeySelector `json:"configMapRef,omitempty"`
}

// SoulSource is non-secret, versioned persona text. ConfigMap sources require an
// expected content digest so replacing a named ConfigMap cannot publish a new soul.
// +kubebuilder:validation:XValidation:rule="(has(self.inline) && self.inline.size() > 0) != has(self.configMapRef)",message="soul must use exactly one of inline or configMapRef"
// +kubebuilder:validation:XValidation:rule="!has(self.configMapRef) || (has(self.digest) && self.digest.size() > 0)",message="ConfigMap-backed souls require an expected digest"
type SoulSource struct {
// Inline is UTF-8 Markdown, limited to 8192 bytes by the controller.
// +kubebuilder:validation:MaxLength=8192
// +optional
Inline string `json:"inline,omitempty"`

// ConfigMapRef selects non-secret text in the Agent's namespace.
// +optional
ConfigMapRef *ConfigMapKeySelector `json:"configMapRef,omitempty"`

// Digest is the expected SHA-256 digest of the exact UTF-8 source bytes.
// Required for ConfigMap sources; optional for inline text.
// +kubebuilder:validation:Pattern=`^sha256:[0-9a-f]{64}$`
// +kubebuilder:validation:MaxLength=71
// +optional
Digest string `json:"digest,omitempty"`
}

// ConfigMapKeySelector selects a key from a ConfigMap
type ConfigMapKeySelector struct {
// Name is the name of the ConfigMap
Expand Down
29 changes: 28 additions & 1 deletion api/v1alpha1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ type SkillReference struct {
// TaskStatus defines the observed state of Task
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.executionOutcome) || self.executionOutcome == oldSelf.executionOutcome",message="executionOutcome is immutable once recorded"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.agentExecutionBinding) || (has(self.agentExecutionBinding) && self.agentExecutionBinding == oldSelf.agentExecutionBinding)",message="agentExecutionBinding is write-once and immutable"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.soulBinding) || (has(self.soulBinding) && self.soulBinding == oldSelf.soulBinding)",message="soulBinding is write-once and immutable"
// +kubebuilder:validation:XValidation: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 v1-bound Task cannot acquire new v2 execution or delivery state"
// +kubebuilder:validation:XValidation:rule="!has(self.agentExecutionBinding) || self.agentExecutionBinding.contractVersion != 'orka.harness.v2' || !has(self.harnessRuntime) || (has(oldSelf.harnessRuntime) && self.harnessRuntime == oldSelf.harnessRuntime)",message="a v2-bound Task cannot acquire new v1 harness state"
type TaskStatus struct {
Expand Down Expand Up @@ -486,6 +487,11 @@ type TaskStatus struct {
// +optional
AgentExecutionBinding *AgentExecutionBinding `json:"agentExecutionBinding,omitempty"`

// SoulBinding pins an AI Task's resolved persona and role prompt. It contains
// digests only, never prompt text. Retries reject configuration drift.
// +optional
SoulBinding *TaskSoulBinding `json:"soulBinding,omitempty"`

// ExecutionOutcome records the immutable outcome of a non-ACP workload before
// provider-neutral execution-workspace finalization completes.
// +optional
Expand Down Expand Up @@ -528,6 +534,27 @@ type TaskStatus struct {
NextScheduleTime *metav1.Time `json:"nextScheduleTime,omitempty"`
}

// TaskSoulBinding is the non-secret, write-once identity of AI prompt configuration.
type TaskSoulBinding struct {
// TaskGeneration is the Task spec generation whose prompt was resolved.
// +kubebuilder:validation:Minimum=1
TaskGeneration int64 `json:"taskGeneration"`
// AgentUID and AgentGeneration identify the exact Agent revision.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=128
AgentUID string `json:"agentUID"`
// +kubebuilder:validation:Minimum=1
AgentGeneration int64 `json:"agentGeneration"`
// SoulDigest identifies the exact persona source bytes.
// +kubebuilder:validation:Pattern=`^sha256:[0-9a-f]{64}$`
// +kubebuilder:validation:MaxLength=71
SoulDigest string `json:"soulDigest"`
// PromptDigest identifies the composed role and persona prompt.
// +kubebuilder:validation:Pattern=`^sha256:[0-9a-f]{64}$`
// +kubebuilder:validation:MaxLength=71
PromptDigest string `json:"promptDigest"`
}

// TaskWorkloadExecutionOutcome records the immutable result of non-ACP workload
// execution before provider-neutral workspace finalization. ACP agent attempts use
// TaskStatus.Execution, whose stronger fencing and OutcomeUnknown semantics are
Expand Down Expand Up @@ -704,7 +731,7 @@ type ChildTaskStatus struct {
// +kubebuilder:printcolumn:name="Priority",type=integer,JSONPath=`.spec.priority`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:selectablefield:JSONPath=.spec.sessionRef.name
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.status) || (!has(oldSelf.status.agentExecutionBinding) || self.spec == oldSelf.spec)",message="Task spec is immutable after execution authority is recorded"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.status) || ((!has(oldSelf.status.agentExecutionBinding) && !has(oldSelf.status.soulBinding)) || self.spec == oldSelf.spec)",message="Task spec is immutable after execution authority is recorded"

// Task is the Schema for the tasks API
type Task struct {
Expand Down
45 changes: 45 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,7 @@ func main() {
BearerTokenFile: acpProviderProxyTokenFile,
}
runtimePoolReconciler.Epochs = controllerEpochManager
runtimePoolReconciler.ControlStore = durableControlStore
runtimePoolReconciler.EnablePDB = true
runtimePoolReconciler.EnableTelemetry = enableTracing
runtimePoolReconciler.E2EPromptWriteAmbiguityMarker = acpE2EPromptWriteAmbiguityMarker
Expand Down
43 changes: 38 additions & 5 deletions config/crd/bases/core.orka.ai_agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,44 @@ spec:
type: string
type: object
type: array
soul:
description: |-
Soul supplies persistent persona and communication defaults, separate from the role prompt.
Supported by AI workers and built-in harness v2 runtimes, not runtimeRef runtimes.
properties:
configMapRef:
description: ConfigMapRef selects non-secret text in the Agent's
namespace.
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
digest:
description: |-
Digest is the expected SHA-256 digest of the exact UTF-8 source bytes.
Required for ConfigMap sources; optional for inline text.
maxLength: 71
pattern: ^sha256:[0-9a-f]{64}$
type: string
inline:
description: Inline is UTF-8 Markdown, limited to 8192 bytes by
the controller.
maxLength: 8192
type: string
type: object
x-kubernetes-validations:
- message: soul must use exactly one of inline or configMapRef
rule: (has(self.inline) && self.inline.size() > 0) != has(self.configMapRef)
- message: ConfigMap-backed souls require an expected digest
rule: '!has(self.configMapRef) || (has(self.digest) && self.digest.size()
> 0)'
systemPrompt:
description: SystemPrompt defines the system prompt configuration
properties:
Expand Down Expand Up @@ -1586,11 +1624,6 @@ spec:
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:
Expand Down
46 changes: 44 additions & 2 deletions config/crd/bases/core.orka.ai_tasks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3262,6 +3262,45 @@ spec:
required:
- available
type: object
soulBinding:
description: |-
SoulBinding pins an AI Task's resolved persona and role prompt. It contains
digests only, never prompt text. Retries reject configuration drift.
properties:
agentGeneration:
format: int64
minimum: 1
type: integer
agentUID:
description: AgentUID and AgentGeneration identify the exact Agent
revision.
maxLength: 128
minLength: 1
type: string
promptDigest:
description: PromptDigest identifies the composed role and persona
prompt.
maxLength: 71
pattern: ^sha256:[0-9a-f]{64}$
type: string
soulDigest:
description: SoulDigest identifies the exact persona source bytes.
maxLength: 71
pattern: ^sha256:[0-9a-f]{64}$
type: string
taskGeneration:
description: TaskGeneration is the Task spec generation whose
prompt was resolved.
format: int64
minimum: 1
type: integer
required:
- agentGeneration
- agentUID
- promptDigest
- soulDigest
- taskGeneration
type: object
startTime:
description: StartTime is when the task started running
format: date-time
Expand All @@ -3277,6 +3316,9 @@ spec:
- message: agentExecutionBinding is write-once and immutable
rule: '!has(oldSelf.agentExecutionBinding) || (has(self.agentExecutionBinding)
&& self.agentExecutionBinding == oldSelf.agentExecutionBinding)'
- message: soulBinding is write-once and immutable
rule: '!has(oldSelf.soulBinding) || (has(self.soulBinding) && self.soulBinding
== oldSelf.soulBinding)'
- message: a v1-bound Task cannot acquire new v2 execution or delivery
state
rule: '!has(self.agentExecutionBinding) || self.agentExecutionBinding.contractVersion
Expand All @@ -3290,8 +3332,8 @@ spec:
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)'
rule: '!has(oldSelf.status) || ((!has(oldSelf.status.agentExecutionBinding)
&& !has(oldSelf.status.soulBinding)) || self.spec == oldSelf.spec)'
selectableFields:
- jsonPath: .spec.sessionRef.name
served: true
Expand Down
Loading
Loading