feat: inherit valueFrom/envFrom env from Deployment; prefer paperclip container

- SelfPodInfo gains inheritedEnvValueFrom (V1EnvVar[]) and inheritedEnvFrom (V1EnvFromSource[])
- Container selection now prefers the container named "paperclip", falls back to first
- buildJobManifest appends valueFrom env vars (skipping names already overridden)
  and sets envFrom on the opencode container when present
- Tests updated: mock updated, 5 new cases covering secretKeyRef forwarding,
  dedup, envFrom passthrough, and empty-envFrom omission

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-24 22:12:31 +00:00
parent 84dc0f5930
commit 61d2a42a66
11 changed files with 1367 additions and 60 deletions
+34 -4
View File
@@ -14,6 +14,8 @@ import {
} from "@paperclipai/adapter-utils/server-utils";
import type { SelfPodInfo } from "./k8s-client.js";
export const LARGE_PROMPT_THRESHOLD_BYTES = 256 * 1024;
export interface JobBuildInput {
ctx: AdapterExecutionContext;
selfPod: SelfPodInfo;
@@ -21,6 +23,12 @@ export interface JobBuildInput {
instructionsContent?: string;
/** Concatenated content of desired skill markdown files, prepended after instructions. */
skillsBundleContent?: string;
/**
* When set, the prompt is stored in this K8s Secret (already created by the caller)
* and the init container mounts and copies it instead of using an env var.
* Required when the prompt exceeds LARGE_PROMPT_THRESHOLD_BYTES.
*/
promptSecretName?: string;
}
export interface JobBuildResult {
@@ -157,12 +165,19 @@ function buildEnvVars(
merged.OPENCODE_DISABLE_PROJECT_CONFIG = "true";
merged.HOME = "/paperclip";
// Convert to V1EnvVar array
// Convert literal-value vars to V1EnvVar array
const envVars: k8s.V1EnvVar[] = Object.entries(merged).map(([name, value]) => ({
name,
value,
}));
// Append valueFrom vars (Secret/ConfigMap-backed) only for names not already overridden
for (const envVar of selfPod.inheritedEnvValueFrom) {
if (!Object.prototype.hasOwnProperty.call(merged, envVar.name)) {
envVars.push({ name: envVar.name, valueFrom: envVar.valueFrom });
}
}
return envVars;
}
@@ -293,6 +308,10 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
const volumes: k8s.V1Volume[] = [{ name: "prompt", emptyDir: {} }];
const volumeMounts: k8s.V1VolumeMount[] = [{ name: "prompt", mountPath: "/tmp/prompt" }];
if (input.promptSecretName) {
volumes.push({ name: "prompt-secret", secret: { secretName: input.promptSecretName } });
}
if (selfPod.pvcClaimName) {
volumes.push({
name: "data",
@@ -370,9 +389,19 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
name: "write-prompt",
image: "busybox:1.36",
imagePullPolicy: "IfNotPresent",
command: ["sh", "-c", "echo \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt"],
env: [{ name: "PROMPT_CONTENT", value: prompt }],
volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }],
...(input.promptSecretName
? {
command: ["sh", "-c", "cp /tmp/prompt-secret/prompt /tmp/prompt/prompt.txt"],
volumeMounts: [
{ name: "prompt", mountPath: "/tmp/prompt" },
{ name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true },
],
}
: {
command: ["sh", "-c", "printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt"],
env: [{ name: "PROMPT_CONTENT", value: prompt }],
volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }],
}),
securityContext,
resources: {
requests: { cpu: "10m", memory: "16Mi" },
@@ -388,6 +417,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
workingDir,
command: ["sh", "-c", mainCommand],
env: envVars,
...(selfPod.inheritedEnvFrom.length > 0 ? { envFrom: selfPod.inheritedEnvFrom } : {}),
volumeMounts,
securityContext,
resources: containerResources,