fix: reserve paperclip.io/ and app.kubernetes.io/ label prefixes (N2)
Co-Authored-By: Claude Sonnet <noreply@anthropic.com> Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -700,6 +700,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const claudeArgs = built.claudeArgs;
|
const claudeArgs = built.claudeArgs;
|
||||||
const promptMetrics = built.promptMetrics;
|
const promptMetrics = built.promptMetrics;
|
||||||
promptSecret = built.promptSecret;
|
promptSecret = built.promptSecret;
|
||||||
|
if (built.skippedLabels.length > 0) {
|
||||||
|
await onLog("stderr", `[paperclip] Warning: skipped ${built.skippedLabels.length} extra label(s) with reserved prefix: ${built.skippedLabels.join(", ")}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
// Report invocation metadata
|
// Report invocation metadata
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
|
|||||||
@@ -166,6 +166,41 @@ describe("buildJobManifest", () => {
|
|||||||
expect(job.metadata?.labels?.["paperclip.io/task-id"]).toBeUndefined();
|
expect(job.metadata?.labels?.["paperclip.io/task-id"]).toBeUndefined();
|
||||||
expect(job.metadata?.labels?.["paperclip.io/session-id"]).toBeUndefined();
|
expect(job.metadata?.labels?.["paperclip.io/session-id"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("drops user label with paperclip.io/ prefix", () => {
|
||||||
|
ctx.config = { labels: { "paperclip.io/run-id": "hijacked" } };
|
||||||
|
const { job, skippedLabels } = buildJobManifest({ ctx, selfPod });
|
||||||
|
expect(job.metadata?.labels?.["paperclip.io/run-id"]).not.toBe("hijacked");
|
||||||
|
expect(skippedLabels).toContain("paperclip.io/run-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops user label with app.kubernetes.io/ prefix", () => {
|
||||||
|
ctx.config = { labels: { "app.kubernetes.io/managed-by": "attacker" } };
|
||||||
|
const { job, skippedLabels } = buildJobManifest({ ctx, selfPod });
|
||||||
|
expect(job.metadata?.labels?.["app.kubernetes.io/managed-by"]).toBe("paperclip");
|
||||||
|
expect(skippedLabels).toContain("app.kubernetes.io/managed-by");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through user label without reserved prefix", () => {
|
||||||
|
ctx.config = { labels: { "custom.io/team": "platform" } };
|
||||||
|
const { job, skippedLabels } = buildJobManifest({ ctx, selfPod });
|
||||||
|
expect(job.metadata?.labels?.["custom.io/team"]).toBe("platform");
|
||||||
|
expect(skippedLabels).not.toContain("custom.io/team");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates skippedLabels with all dropped keys", () => {
|
||||||
|
ctx.config = {
|
||||||
|
labels: {
|
||||||
|
"paperclip.io/agent-id": "x",
|
||||||
|
"app.kubernetes.io/component": "y",
|
||||||
|
"safe": "z",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { skippedLabels } = buildJobManifest({ ctx, selfPod });
|
||||||
|
expect(skippedLabels).toHaveLength(2);
|
||||||
|
expect(skippedLabels).toContain("paperclip.io/agent-id");
|
||||||
|
expect(skippedLabels).toContain("app.kubernetes.io/component");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("annotations", () => {
|
describe("annotations", () => {
|
||||||
|
|||||||
@@ -200,6 +200,8 @@ export interface JobBuildResult {
|
|||||||
/** Non-null when the prompt is too large for an env var and must be
|
/** Non-null when the prompt is too large for an env var and must be
|
||||||
* staged as a K8s Secret before creating the Job. */
|
* staged as a K8s Secret before creating the Job. */
|
||||||
promptSecret: PromptSecret | null;
|
promptSecret: PromptSecret | null;
|
||||||
|
/** User-supplied extra labels that were dropped because they used a reserved prefix. */
|
||||||
|
skippedLabels: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeForK8sName(value: string, maxLen = 16): string {
|
function sanitizeForK8sName(value: string, maxLen = 16): string {
|
||||||
@@ -460,8 +462,13 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
|||||||
if (taskLabel) labels["paperclip.io/task-id"] = taskLabel;
|
if (taskLabel) labels["paperclip.io/task-id"] = taskLabel;
|
||||||
const sessionLabel = runtimeSessionId ? sanitizeLabelValue(runtimeSessionId) : null;
|
const sessionLabel = runtimeSessionId ? sanitizeLabelValue(runtimeSessionId) : null;
|
||||||
if (sessionLabel) labels["paperclip.io/session-id"] = sessionLabel;
|
if (sessionLabel) labels["paperclip.io/session-id"] = sessionLabel;
|
||||||
|
const skippedLabels: string[] = [];
|
||||||
for (const [key, value] of Object.entries(extraLabels)) {
|
for (const [key, value] of Object.entries(extraLabels)) {
|
||||||
labels[key] = value;
|
if (key.startsWith("paperclip.io/") || key.startsWith("app.kubernetes.io/")) {
|
||||||
|
skippedLabels.push(key);
|
||||||
|
} else {
|
||||||
|
labels[key] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volumes
|
// Volumes
|
||||||
@@ -628,5 +635,5 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { job, jobName, namespace, prompt, claudeArgs, promptMetrics, promptSecret };
|
return { job, jobName, namespace, prompt, claudeArgs, promptMetrics, promptSecret, skippedLabels };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user