Skip to content

Commit 7583a60

Browse files
authored
feat: configure commit signing on agent runs (#737)
* configure commit signing on agent runs * fix signing key * code review * move to common * linter * pin codex version * linter * update version * fix get secret * update codex template * linter * pin codex version * update codex * linter * fix signing key * fix permission to signing key * fix permission to signing key * change permission * change permission * test codex
1 parent 1d3bfa1 commit 7583a60

15 files changed

Lines changed: 418 additions & 19 deletions

File tree

Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ PLRL_GEMINI_MODEL := $(if $(PLRL_GEMINI_MODEL),$(PLRL_GEMINI_MODEL),"")
2424
PLRL_GEMINI_API_KEY := $(if $(PLRL_GEMINI_API_KEY),$(PLRL_GEMINI_API_KEY),"")
2525
PLRL_CODEX_MODEL := $(if $(PLRL_CODEX_MODEL),$(PLRL_CODEX_MODEL),"")
2626
PLRL_CODEX_API_KEY := $(if $(PLRL_CODEX_API_KEY),$(PLRL_CODEX_API_KEY),"")
27+
GIT_ACCESS_TOKEN := $(if $(GIT_ACCESS_TOKEN),$(GIT_ACCESS_TOKEN),"")
2728

2829

2930
VELERO_CHART_VERSION := 5.2.2 # It should be kept in sync with Velero chart version from console/charts/velero
@@ -147,14 +148,20 @@ agent-harness-opencode-run: docker-build-agent-harness-opencode ## run agent har
147148

148149
.PHONY: agent-harness-codex-run
149150
agent-harness-codex-run: docker-build-agent-harness-codex ## run agent harness w/ codex
151+
@KEY_TMP=$$(mktemp) && \
152+
cp $${HOME}/.ssh/id_rsa $$KEY_TMP && \
153+
chmod 644 $$KEY_TMP && \
150154
docker run \
155+
-v $$KEY_TMP:/plural/git/git-signing.key:ro \
151156
-e PLRL_AGENT_RUN_ID=$(PLRL_AGENT_RUN_ID) \
152157
-e PLRL_DEPLOY_TOKEN=$(PLRL_DEPLOY_TOKEN) \
153158
-e PLRL_CONSOLE_URL=$(PLRL_CONSOLE_URL) \
154159
-e PLRL_CODEX_MODEL=$(PLRL_CODEX_MODEL) \
155160
-e PLRL_CODEX_API_KEY=$(PLRL_CODEX_API_KEY) \
161+
-e GIT_ACCESS_TOKEN=$(GIT_ACCESS_TOKEN) \
156162
--rm -it \
157-
ghcr.io/pluralsh/agent-harness-codex --v=3
163+
ghcr.io/pluralsh/agent-harness-codex --v=3; \
164+
rm -f $$KEY_TMP
158165

159166
.PHONY: agent-harness-claude-run
160167
agent-harness-claude-run: docker-build-agent-harness-claude ## run agent harness w/ claude
@@ -333,7 +340,7 @@ docker-build-agent-harness-claude: docker-build-agent-harness-base ## build clau
333340

334341
.PHONY: docker-build-agent-harness-codex
335342
docker-build-agent-harness-codex: docker-build-agent-harness-base ## build codex docker agent harness image
336-
docker build \
343+
docker build --no-cache \
337344
--build-arg=AGENT_HARNESS_BASE_IMAGE_TAG="latest" \
338345
-t ghcr.io/pluralsh/agent-harness-codex \
339346
-f dockerfiles/agent-harness/codex.Dockerfile \

api/v1alpha1/agentruntime_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ type AgentRuntimeSpec struct {
6767
// configure tooling, or perform any other setup required by the agent.
6868
// +kubebuilder:validation:Optional
6969
BootstrapScript *string `json:"bootstrapScript,omitempty"`
70+
71+
// Git configure commit signing on agent run. When provided, the runtime will be configured to sign git commits using the provided key reference.
72+
Git *GitSpec `json:"git,omitempty"`
73+
}
74+
75+
type GitSpec struct {
76+
Proxy *string `json:"proxy,omitempty"`
77+
SigningKeyRef *corev1.SecretKeySelector `json:"signingKeyRef,omitempty"`
7078
}
7179

7280
// Browser defines the browser to use for the agent runtime.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/deployment-operator/crds/deployments.plural.sh_agentruntimes.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1867,6 +1867,38 @@ spec:
18671867
Dind enables Docker-in-Docker for this agent runtime.
18681868
When true, the runtime will be configured to run with DinD support.
18691869
type: boolean
1870+
git:
1871+
description: Git configure commit signing on agent run. When provided,
1872+
the runtime will be configured to sign git commits using the provided
1873+
key reference.
1874+
properties:
1875+
proxy:
1876+
type: string
1877+
signingKeyRef:
1878+
description: SecretKeySelector selects a key of a Secret.
1879+
properties:
1880+
key:
1881+
description: The key of the secret to select from. Must be
1882+
a valid secret key.
1883+
type: string
1884+
name:
1885+
default: ""
1886+
description: |-
1887+
Name of the referent.
1888+
This field is effectively required, but due to backwards compatibility is
1889+
allowed to be empty. Instances of this type with an empty value here are
1890+
almost certainly wrong.
1891+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
1892+
type: string
1893+
optional:
1894+
description: Specify whether the Secret or its key must be
1895+
defined
1896+
type: boolean
1897+
required:
1898+
- key
1899+
type: object
1900+
x-kubernetes-map-type: atomic
1901+
type: object
18701902
name:
18711903
description: |-
18721904
Name of this AgentRuntime.

config/crd/bases/deployments.plural.sh_agentruntimes.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1867,6 +1867,38 @@ spec:
18671867
Dind enables Docker-in-Docker for this agent runtime.
18681868
When true, the runtime will be configured to run with DinD support.
18691869
type: boolean
1870+
git:
1871+
description: Git configure commit signing on agent run. When provided,
1872+
the runtime will be configured to sign git commits using the provided
1873+
key reference.
1874+
properties:
1875+
proxy:
1876+
type: string
1877+
signingKeyRef:
1878+
description: SecretKeySelector selects a key of a Secret.
1879+
properties:
1880+
key:
1881+
description: The key of the secret to select from. Must be
1882+
a valid secret key.
1883+
type: string
1884+
name:
1885+
default: ""
1886+
description: |-
1887+
Name of the referent.
1888+
This field is effectively required, but due to backwards compatibility is
1889+
allowed to be empty. Instances of this type with an empty value here are
1890+
almost certainly wrong.
1891+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
1892+
type: string
1893+
optional:
1894+
description: Specify whether the Secret or its key must be
1895+
defined
1896+
type: boolean
1897+
required:
1898+
- key
1899+
type: object
1900+
x-kubernetes-map-type: atomic
1901+
type: object
18701902
name:
18711903
description: |-
18721904
Name of this AgentRuntime.

dockerfiles/agent-harness/codex.Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
ARG NODE_IMAGE_TAG=24
22
ARG NODE_IMAGE=node:${NODE_IMAGE_TAG}-slim
3-
ARG AGENT_VERSION=latest
3+
ARG AGENT_VERSION=0.104.0
44

55
ARG AGENT_HARNESS_BASE_IMAGE_TAG=latest
66
ARG AGENT_HARNESS_BASE_IMAGE_REPO=ghcr.io/pluralsh/agent-harness-base
@@ -12,7 +12,7 @@ FROM $NODE_IMAGE AS node
1212
USER root
1313

1414
# Install codex CLI globally using npm
15-
RUN npm install -g @openai/codex@$AGENT_VERSION
15+
RUN npm install -g @openai/codex@0.104.0
1616

1717
# The codex script uses createRequire(import.meta.url) anchored at /usr/local/bin/codex.
1818
# Node's module resolution walks up from /usr/local/bin/ and won't find node_modules

docs/api.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ _Appears in:_
268268
| `allowedRepositories` _string array_ | AllowedRepositories the git repositories allowed to be used with this runtime. | | Optional: \{\} <br /> |
269269
| `browser` _[BrowserConfig](#browserconfig)_ | Browser configuration augments agent runtime with a headless browser.<br />When provided, the runtime will be configured to run with a headless browser available<br />for the agent to use. | | Optional: \{\} <br /> |
270270
| `bootstrapScript` _string_ | BootstrapScript is a bash script that will be executed inside the cloned repository<br />directory before the coding agent starts. It can be used to install dependencies,<br />configure tooling, or perform any other setup required by the agent. | | Optional: \{\} <br /> |
271+
| `git` _[GitSpec](#gitspec)_ | Git configure commit signing on agent run. When provided, the runtime will be configured to sign git commits using the provided key reference. | | |
271272

272273

273274
#### Binding
@@ -656,6 +657,23 @@ _Appears in:_
656657
| `inactivityTimeout` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | InactivityTimeout is the timeout for inactivity during gemini run. | | Optional: \{\} <br /> |
657658

658659

660+
#### GitSpec
661+
662+
663+
664+
665+
666+
667+
668+
_Appears in:_
669+
- [AgentRuntimeSpec](#agentruntimespec)
670+
671+
| Field | Description | Default | Validation |
672+
| --- | --- | --- | --- |
673+
| `proxy` _string_ | | | |
674+
| `signingKeyRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | | | |
675+
676+
659677

660678

661679
#### HelmConfiguration

internal/controller/agentrun_controller.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const (
5858
EnvDindEnabled = "PLRL_DIND_ENABLED"
5959
EnvBrowserEnabled = "PLRL_BROWSER_ENABLED"
6060
EnvExecTimeout = "PLRL_EXEC_TIMEOUT"
61+
62+
EnvGitProxy = "PLRL_GIT_PROXY"
6163
)
6264

6365
var (
@@ -358,6 +360,11 @@ func (r *AgentRunReconciler) reconcilePodSecret(ctx context.Context, run *v1alph
358360
return nil, fmt.Errorf("failed to get agent runtime config: %w", err)
359361
}
360362

363+
signingKey, err := r.resolveSigningKey(ctx, runtime)
364+
if err != nil {
365+
return nil, fmt.Errorf("failed to resolve git signing key: %w", err)
366+
}
367+
361368
secret := &corev1.Secret{}
362369
if err := r.Get(ctx, types.NamespacedName{Name: run.Name, Namespace: run.Namespace}, secret); err != nil {
363370
if !errors.IsNotFound(err) {
@@ -366,7 +373,7 @@ func (r *AgentRunReconciler) reconcilePodSecret(ctx context.Context, run *v1alph
366373

367374
secret = &corev1.Secret{
368375
ObjectMeta: metav1.ObjectMeta{Name: run.Name, Namespace: run.Namespace},
369-
StringData: r.getSecretData(run, config, runtime.Spec.Type),
376+
StringData: r.getSecretData(run, config, runtime.Spec.Type, signingKey),
370377
}
371378

372379
logger.V(2).Info("creating secret", "namespace", secret.Namespace, "name", secret.Name)
@@ -379,7 +386,7 @@ func (r *AgentRunReconciler) reconcilePodSecret(ctx context.Context, run *v1alph
379386

380387
if !r.hasSecretData(secret.Data, run) {
381388
logger.V(2).Info("updating secret", "namespace", secret.Namespace, "name", secret.Name)
382-
secret.StringData = r.getSecretData(run, config, runtime.Spec.Type)
389+
secret.StringData = r.getSecretData(run, config, runtime.Spec.Type, signingKey)
383390
if err := r.Update(ctx, secret); err != nil {
384391
logger.Error(err, "unable to update secret")
385392
return nil, err
@@ -388,7 +395,6 @@ func (r *AgentRunReconciler) reconcilePodSecret(ctx context.Context, run *v1alph
388395

389396
return secret, nil
390397
}
391-
392398
func (r *AgentRunReconciler) getAgentRuntimeConfig(ctx context.Context, namespace string, config *v1alpha1.AgentRuntimeConfig) (*v1alpha1.AgentRuntimeConfigRaw, error) {
393399
if config == nil {
394400
return nil, nil
@@ -401,13 +407,39 @@ func (r *AgentRunReconciler) getAgentRuntimeConfig(ctx context.Context, namespac
401407
})
402408
}
403409

404-
func (r *AgentRunReconciler) getSecretData(run *v1alpha1.AgentRun, config *v1alpha1.AgentRuntimeConfigRaw, runtimeType console.AgentRuntimeType) map[string]string {
410+
// resolveSigningKey fetches the signing key value from the secret referenced in runtime.Spec.Git.SigningKeyRef.
411+
// It looks up the secret in the AgentRuntime's own namespace so that the source secret does not need
412+
// to exist in the pod's TargetNamespace. The returned bytes are later copied into the pod secret.
413+
func (r *AgentRunReconciler) resolveSigningKey(ctx context.Context, runtime *v1alpha1.AgentRuntime) ([]byte, error) {
414+
if runtime.Spec.Git == nil || runtime.Spec.Git.SigningKeyRef == nil {
415+
return nil, nil
416+
}
417+
418+
ref := runtime.Spec.Git.SigningKeyRef
419+
s := &corev1.Secret{}
420+
if err := r.Get(ctx, client.ObjectKey{Namespace: runtime.Spec.TargetNamespace, Name: ref.Name}, s); err != nil {
421+
return nil, fmt.Errorf("failed to get git signing key secret %q: %w", ref.Name, err)
422+
}
423+
424+
value, ok := s.Data[ref.Key]
425+
if !ok {
426+
return nil, fmt.Errorf("key %q not found in secret %q", ref.Key, ref.Name)
427+
}
428+
429+
return value, nil
430+
}
431+
432+
func (r *AgentRunReconciler) getSecretData(run *v1alpha1.AgentRun, config *v1alpha1.AgentRuntimeConfigRaw, runtimeType console.AgentRuntimeType, signingKey []byte) map[string]string {
405433
result := map[string]string{
406434
EnvConsoleURL: r.ConsoleURL,
407435
EnvDeployToken: r.DeployToken,
408436
EnvAgentRunID: run.Status.GetID(),
409437
}
410438

439+
if len(signingKey) > 0 {
440+
result[gitSigningKeySecretKey] = string(signingKey)
441+
}
442+
411443
if config == nil {
412444
return result
413445
}

internal/controller/agentrun_controller_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ var _ = Describe("AgentRun Controller", Ordered, func() {
606606
run := &v1alpha1.AgentRun{}
607607
run.Status.ID = lo.ToPtr("run-456")
608608

609-
data := reconciler.getSecretData(run, nil, console.AgentRuntimeTypeClaude)
609+
data := reconciler.getSecretData(run, nil, console.AgentRuntimeTypeClaude, nil)
610610
Expect(data).Should(HaveLen(3))
611611
Expect(data[EnvConsoleURL]).Should(Equal("https://console.test.com"))
612612
Expect(data[EnvDeployToken]).Should(Equal("test-token-123"))
@@ -662,7 +662,7 @@ var _ = Describe("AgentRun Controller", Ordered, func() {
662662
},
663663
}
664664

665-
data := reconciler.getSecretData(run, config, console.AgentRuntimeTypeClaude)
665+
data := reconciler.getSecretData(run, config, console.AgentRuntimeTypeClaude, nil)
666666
Expect(data[EnvClaudeModel]).Should(Equal("claude-3-opus"))
667667
Expect(data[EnvClaudeToken]).Should(Equal("claude-api-key"))
668668
Expect(data[EnvClaudeArgs]).Should(ContainSubstring("--verbose"))
@@ -686,7 +686,7 @@ var _ = Describe("AgentRun Controller", Ordered, func() {
686686
},
687687
}
688688

689-
data := reconciler.getSecretData(run, config, console.AgentRuntimeTypeOpencode)
689+
data := reconciler.getSecretData(run, config, console.AgentRuntimeTypeOpencode, nil)
690690
Expect(data[EnvOpenCodeProvider]).Should(Equal("openai"))
691691
Expect(data[EnvOpenCodeEndpoint]).Should(Equal("https://api.openai.com"))
692692
Expect(data[EnvOpenCodeModel]).Should(Equal("gpt-4"))
@@ -708,7 +708,7 @@ var _ = Describe("AgentRun Controller", Ordered, func() {
708708
},
709709
}
710710

711-
data := reconciler.getSecretData(run, config, console.AgentRuntimeTypeGemini)
711+
data := reconciler.getSecretData(run, config, console.AgentRuntimeTypeGemini, nil)
712712
Expect(data[EnvGeminiModel]).Should(Equal("gemini-pro"))
713713
Expect(data[EnvGeminiAPIKey]).Should(Equal("gemini-api-key"))
714714
})

0 commit comments

Comments
 (0)