Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions docs/architecture/configuration-and-compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ with `tools[].subAgent.templateRef` and compile under the parent Agent's Harness
Each subagent requires `templateRef` and shares the parent's runtime and Harness.
Dedicated `agentRef` bindings are deferred and are not part of the served API.

`Agent.spec.egress` lists HTTP(S) origins the Agent may reach besides the
destinations its revision compiles, such as `https://proxy.golang.org`. A host
may have `*` as its leftmost label (`https://*.githubusercontent.com` matches
one label), with at least two labels under it, so nothing opens a top-level
domain or every host. The field is on the Agent, not the
template or the Harness: the hosts are a property of the workload in the place
where it runs (an internal git host, a registry mirror), so a template stays
portable and a Harness stays about the runtime. The compiler adds the origins,
in canonical form, after the runtime compiler has run, so every runtime and
every Shared subagent treats them the same, and they are part of the revision
identity.

An entry on an exact `https` origin may list `headers`, each from a key of a
Secret in the Agent's namespace. They compile to gateway credential bindings
like ModelConfig keys (see [credential injection](credential-injection.md)).
A missing Secret or key reports `SecretNotFound` or `SecretKeyNotFound`.

All three are `api.kagent.dev/v1alpha3` Kubernetes resources. Infrastructure-derived
values such as runtime addresses and inferred egress do not belong in the public
API.
Expand Down
7 changes: 6 additions & 1 deletion docs/architecture/credential-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ overrides of these variables are rejected.
| Gemini API key | `x-goog-api-key: <key>` |
| Bedrock bearer token | `authorization: Bearer <token>` |
| RemoteMCPServer Secret-backed header | Configured header; Secret contains its full value |
| Agent or SandboxTemplate egress header (`spec.egress[].headers`) | Configured header and optional prefix; Secret contains the rest of the value |

Provider endpoint overrides determine the allowed HTTP(S) origin. Egress rules
match its scheme, DNS name, and port; credential bindings remain scoped to the
Expand All @@ -67,7 +68,11 @@ by Substrate egress policies.

Harness and SandboxTemplate environment entries accept only literal `value`
strings, including empty strings. Configure Secret-backed credentials on
ModelConfig or RemoteMCPServer for gateway injection.
ModelConfig, RemoteMCPServer, or Agent or SandboxTemplate egress headers for
gateway injection. Egress headers are also rejected on a host the runtime
reaches over HTTPS on another port. A sandbox's headers resolve Secrets in its
SandboxTemplate's namespace, under the same Substrate credential policy as
Agents there.

AWS IAM signing keys, Google service-account keys, and OAuth client credentials
require mechanisms beyond static header injection and are rejected rather than
Expand Down
10 changes: 8 additions & 2 deletions docs/architecture/sandboxes.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,14 @@ TTL. Helm supplies these through the controller ConfigMap. Defaults are one CPU,
1 GiB memory, a one-hour TTL, and a 24-hour maximum TTL. Activity does not extend
expiration.

Sandboxes have no allowed egress destinations. A follow-up will add destination
configuration to `SandboxTemplate` and pin it with each prepared revision.
`SandboxTemplate.spec.egress` lists the HTTP(S) origins a sandbox may reach, in
the same form as `Agent.spec.egress`: origins, with optional Secret-backed
headers the egress gateway sets (see [credential injection](credential-injection.md)).
A template without it reaches nothing. Egress is part of the prepared revision,
and a sandbox gets its revision's policy when it is created, so a template
change applies to new sandboxes; existing ones keep the egress they started with.
A header whose Secret or key is missing holds preparation on `SecretNotFound` or
`SecretKeyNotFound` until it appears.

## Lifecycle and recovery

Expand Down
103 changes: 103 additions & 0 deletions go/api/config/crd/bases/api.kagent.dev_agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,109 @@ spec:
AgentSpec pairs portable behavior with a runner. References are local to the
Agent's namespace, including references nested in inline specs.
properties:
egress:
description: |-
Egress lists HTTP(S) origins the Agent may reach besides the destinations
its revision compiles (the model, its MCP servers, its skill and plugin
sources, telemetry), such as "https://proxy.golang.org". The host may
have "*" as its leftmost label: "https://*.githubusercontent.com" matches
one label, such as raw.githubusercontent.com. A wildcard needs two labels
under it, so nothing opens a whole top-level domain or every host.
items:
description: EgressEntry is one origin a runtime may reach.
properties:
headers:
description: |-
Headers the egress gateway sets, so the runtime never holds their
values. The gateway only replaces a header the request sends, so the
runtime sends each with a placeholder. Credentials bind per host, not
per port. Whoever can write the resource can send any Secret in its
namespace to the origin.
items:
description: EgressHeader is a request header the egress gateway
sets.
properties:
name:
maxLength: 256
pattern: ^[!#$%&'*+.^_|~0-9A-Za-z-]+$
type: string
prefix:
description: Prefix goes before the value, such as "Bearer
".
maxLength: 64
pattern: ^[^\x00-\x08\x0a-\x1f\x7f]*$
type: string
valueFrom:
description: EgressHeaderSource is where a header's value
comes from.
properties:
secretKeyRef:
description: SecretKeyRef is a key of a Secret in
the resource'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
x-kubernetes-validations:
- message: secretKeyRef needs a name and a key
rule: self.name != '' && self.key != ''
- message: a header's Secret cannot be optional
rule: '!has(self.optional) || !self.optional'
type: object
x-kubernetes-validations:
- message: secretKeyRef is required
rule: has(self.secretKeyRef)
required:
- name
- valueFrom
type: object
x-kubernetes-validations:
- message: the gateway cannot set the Host header
rule: self.name.lowerAscii() != 'host'
maxItems: 16
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
x-kubernetes-validations:
- message: header names must be unique, ignoring case
rule: self.all(a, self.exists_one(b, a.name.lowerAscii() ==
b.name.lowerAscii()))
origin:
description: Origin is an HTTP(S) origin, such as "https://proxy.golang.org".
maxLength: 270
pattern: ^https?://(\*\.([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.)+|([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.)*)[a-z]([-a-z0-9]{0,61}[a-z0-9])?(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$
type: string
required:
- origin
type: object
x-kubernetes-validations:
- message: headers need an exact https origin
rule: '!has(self.headers) || (self.origin.startsWith(''https://'')
&& !self.origin.contains(''*''))'
maxItems: 64
type: array
x-kubernetes-list-map-keys:
- origin
x-kubernetes-list-type: map
harness:
description: HarnessSpec defines a reusable runtime and its infrastructure
policy.
Expand Down
99 changes: 99 additions & 0 deletions go/api/config/crd/bases/api.kagent.dev_sandboxtemplates.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,105 @@ spec:
description: SandboxTemplateSpec defines standalone sandbox environment
configuration.
properties:
egress:
description: |-
Egress lists the HTTP(S) origins a sandbox may reach, in the same form as
an Agent's. A sandbox keeps the egress it was created with.
items:
description: EgressEntry is one origin a runtime may reach.
properties:
headers:
description: |-
Headers the egress gateway sets, so the runtime never holds their
values. The gateway only replaces a header the request sends, so the
runtime sends each with a placeholder. Credentials bind per host, not
per port. Whoever can write the resource can send any Secret in its
namespace to the origin.
items:
description: EgressHeader is a request header the egress gateway
sets.
properties:
name:
maxLength: 256
pattern: ^[!#$%&'*+.^_|~0-9A-Za-z-]+$
type: string
prefix:
description: Prefix goes before the value, such as "Bearer
".
maxLength: 64
pattern: ^[^\x00-\x08\x0a-\x1f\x7f]*$
type: string
valueFrom:
description: EgressHeaderSource is where a header's value
comes from.
properties:
secretKeyRef:
description: SecretKeyRef is a key of a Secret in
the resource'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
x-kubernetes-validations:
- message: secretKeyRef needs a name and a key
rule: self.name != '' && self.key != ''
- message: a header's Secret cannot be optional
rule: '!has(self.optional) || !self.optional'
type: object
x-kubernetes-validations:
- message: secretKeyRef is required
rule: has(self.secretKeyRef)
required:
- name
- valueFrom
type: object
x-kubernetes-validations:
- message: the gateway cannot set the Host header
rule: self.name.lowerAscii() != 'host'
maxItems: 16
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
x-kubernetes-validations:
- message: header names must be unique, ignoring case
rule: self.all(a, self.exists_one(b, a.name.lowerAscii() ==
b.name.lowerAscii()))
origin:
description: Origin is an HTTP(S) origin, such as "https://proxy.golang.org".
maxLength: 270
pattern: ^https?://(\*\.([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.)+|([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.)*)[a-z]([-a-z0-9]{0,61}[a-z0-9])?(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$
type: string
required:
- origin
type: object
x-kubernetes-validations:
- message: headers need an exact https origin
rule: '!has(self.headers) || (self.origin.startsWith(''https://'')
&& !self.origin.contains(''*''))'
maxItems: 64
type: array
x-kubernetes-list-map-keys:
- origin
x-kubernetes-list-type: map
env:
description: |-
Env supplies runtime environment defaults.
Expand Down
58 changes: 58 additions & 0 deletions go/api/v1alpha3/agent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,64 @@ type AgentSpec struct {
// +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="harnessRef.name must not be empty"
// +optional
HarnessRef *corev1.LocalObjectReference `json:"harnessRef,omitempty"`
// Egress lists HTTP(S) origins the Agent may reach besides the destinations
// its revision compiles (the model, its MCP servers, its skill and plugin
// sources, telemetry), such as "https://proxy.golang.org". The host may
// have "*" as its leftmost label: "https://*.githubusercontent.com" matches
// one label, such as raw.githubusercontent.com. A wildcard needs two labels
// under it, so nothing opens a whole top-level domain or every host.
// +optional
// +listType=map
// +listMapKey=origin
// +kubebuilder:validation:MaxItems=64
Egress []EgressEntry `json:"egress,omitempty"`
}

// EgressEntry is one origin a runtime may reach.
// +kubebuilder:validation:XValidation:rule="!has(self.headers) || (self.origin.startsWith('https://') && !self.origin.contains('*'))",message="headers need an exact https origin"
type EgressEntry struct {
// Origin is an HTTP(S) origin, such as "https://proxy.golang.org".
// +required
// +kubebuilder:validation:MaxLength=270
// +kubebuilder:validation:Pattern=`^https?://(\*\.([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.)+|([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.)*)[a-z]([-a-z0-9]{0,61}[a-z0-9])?(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$`
Origin string `json:"origin"`
// Headers the egress gateway sets, so the runtime never holds their
// values. The gateway only replaces a header the request sends, so the
// runtime sends each with a placeholder. Credentials bind per host, not
// per port. Whoever can write the resource can send any Secret in its
// namespace to the origin.
// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=16
// +kubebuilder:validation:XValidation:rule="self.all(a, self.exists_one(b, a.name.lowerAscii() == b.name.lowerAscii()))",message="header names must be unique, ignoring case"
Headers []EgressHeader `json:"headers,omitempty"`
}

// EgressHeader is a request header the egress gateway sets.
// +kubebuilder:validation:XValidation:rule="self.name.lowerAscii() != 'host'",message="the gateway cannot set the Host header"
type EgressHeader struct {
// +required
// +kubebuilder:validation:MaxLength=256
// +kubebuilder:validation:Pattern=`^[!#$%&'*+.^_|~0-9A-Za-z-]+$`
Name string `json:"name"`
// Prefix goes before the value, such as "Bearer ".
// +optional
// +kubebuilder:validation:MaxLength=64
// +kubebuilder:validation:Pattern=`^[^\x00-\x08\x0a-\x1f\x7f]*$`
Prefix string `json:"prefix,omitempty"`
// +required
ValueFrom EgressHeaderSource `json:"valueFrom"`
}

// EgressHeaderSource is where a header's value comes from.
// +kubebuilder:validation:XValidation:rule="has(self.secretKeyRef)",message="secretKeyRef is required"
type EgressHeaderSource struct {
// SecretKeyRef is a key of a Secret in the resource's namespace.
// +optional
// +kubebuilder:validation:XValidation:rule="self.name != '' && self.key != ''",message="secretKeyRef needs a name and a key"
// +kubebuilder:validation:XValidation:rule="!has(self.optional) || !self.optional",message="a header's Secret cannot be optional"
SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"`
}

const (
Expand Down
Loading
Loading