feat(plugin): add kubernetes sandbox provider

This commit is contained in:
Dotta
2026-05-12 12:13:07 -05:00
committed by Chris Farhood
parent 55d6c5bfa4
commit 163e3ca1a5
42 changed files with 4168 additions and 0 deletions
@@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest";
import { getAdapterDefaults, KNOWN_ADAPTER_TYPES } from "../../src/adapter-defaults.js";
describe("adapter-defaults", () => {
it("returns defaults for claude_local", () => {
const d = getAdapterDefaults("claude_local");
expect(d.runtimeImage).toBe("ghcr.io/paperclipai/agent-runtime-claude:v1");
expect(d.envKeys).toContain("ANTHROPIC_API_KEY");
expect(d.allowFqdns).toContain("api.anthropic.com");
expect(d.probeCommand).toEqual(["claude", "--version"]);
});
it("returns defaults for codex_local", () => {
const d = getAdapterDefaults("codex_local");
expect(d.runtimeImage).toBe("ghcr.io/paperclipai/agent-runtime-codex:v1");
expect(d.envKeys).toContain("OPENAI_API_KEY");
expect(d.probeCommand).toEqual(["codex", "--version"]);
});
it("throws on unknown adapter type", () => {
expect(() => getAdapterDefaults("nonexistent_local")).toThrow(/unknown adapter type/i);
});
it("KNOWN_ADAPTER_TYPES contains all 7 supported adapters", () => {
expect(KNOWN_ADAPTER_TYPES).toEqual(
new Set([
"claude_local",
"codex_local",
"gemini_local",
"cursor_local",
"opencode_local",
"acpx_local",
"pi_local",
]),
);
});
});
@@ -0,0 +1,60 @@
import { describe, it, expect } from "vitest";
import { buildCiliumNetworkPolicyManifest } from "../../src/cilium-network-policy.js";
describe("buildCiliumNetworkPolicyManifest", () => {
const baseInput = {
namespace: "paperclip-acme",
paperclipServerNamespace: "paperclip",
egressAllowFqdns: ["api.anthropic.com"],
egressAllowCidrs: [] as string[],
};
it("returns a CiliumNetworkPolicy with the correct apiVersion and kind", () => {
const cnp = buildCiliumNetworkPolicyManifest(baseInput);
expect(cnp.apiVersion).toBe("cilium.io/v2");
expect(cnp.kind).toBe("CiliumNetworkPolicy");
});
it("targets agent pods by role label", () => {
const cnp = buildCiliumNetworkPolicyManifest(baseInput);
expect(cnp.spec.endpointSelector.matchLabels["paperclip.io/role"]).toBe("agent");
});
it("includes an FQDN allow rule for each adapter FQDN", () => {
const cnp = buildCiliumNetworkPolicyManifest({
...baseInput,
egressAllowFqdns: ["api.anthropic.com", "api.openai.com"],
});
const fqdnRule = cnp.spec.egress.find((e: { toFQDNs?: { matchName: string }[] }) => e.toFQDNs);
expect(fqdnRule).toBeDefined();
expect(fqdnRule.toFQDNs.map((f: { matchName: string }) => f.matchName).sort()).toEqual([
"api.anthropic.com",
"api.openai.com",
]);
});
it("permits DNS to kube-dns explicitly so FQDN resolution can happen", () => {
const cnp = buildCiliumNetworkPolicyManifest(baseInput);
const dnsRule = cnp.spec.egress.find((e: { toPorts?: { ports: { port: string }[] }[] }) =>
e.toPorts?.some((tp) => tp.ports.some((p) => p.port === "53")),
);
expect(dnsRule).toBeDefined();
});
it("includes a rule for paperclip-server callback", () => {
const cnp = buildCiliumNetworkPolicyManifest(baseInput);
const cb = cnp.spec.egress.find((e: { toEndpoints?: { matchLabels: Record<string, string> }[] }) =>
e.toEndpoints?.some((ep) => ep.matchLabels.app === "paperclip-server"),
);
expect(cb).toBeDefined();
});
it("includes user-supplied CIDRs in toCIDRSet rule", () => {
const cnp = buildCiliumNetworkPolicyManifest({
...baseInput,
egressAllowCidrs: ["10.0.0.0/8"],
});
const cidrRule = cnp.spec.egress.find((e: { toCIDRSet?: { cidr: string }[] }) => e.toCIDRSet);
expect(cidrRule.toCIDRSet[0].cidr).toBe("10.0.0.0/8");
});
});
@@ -0,0 +1,62 @@
import { describe, it, expect } from "vitest";
import { globMatch, resolveImage } from "../../src/image-allowlist.js";
describe("globMatch", () => {
it("matches exact image", () => {
expect(globMatch("ghcr.io/paperclipai/agent-runtime-claude:v1", "ghcr.io/paperclipai/agent-runtime-claude:v1")).toBe(true);
});
it("matches single-character wildcard", () => {
expect(globMatch("ghcr.io/x:v?", "ghcr.io/x:v1")).toBe(true);
expect(globMatch("ghcr.io/x:v?", "ghcr.io/x:v12")).toBe(false);
});
it("matches multi-character wildcard", () => {
expect(globMatch("ghcr.io/paperclipai/*:v1", "ghcr.io/paperclipai/agent-runtime-claude:v1")).toBe(true);
expect(globMatch("ghcr.io/paperclipai/*:v1", "docker.io/other/img:v1")).toBe(false);
});
it("does not allow wildcard to span slashes by default", () => {
expect(globMatch("ghcr.io/*:v1", "ghcr.io/paperclipai/agent-runtime-claude:v1")).toBe(false);
});
});
describe("resolveImage", () => {
const defaults = { runtimeImage: "ghcr.io/paperclipai/agent-runtime-claude:v1" };
it("uses adapter default when no override", () => {
expect(resolveImage({ imageOverride: null }, defaults, { imageAllowList: [], imageRegistry: undefined })).toBe(
"ghcr.io/paperclipai/agent-runtime-claude:v1",
);
});
it("rewrites registry when imageRegistry is set", () => {
expect(
resolveImage(
{ imageOverride: null },
defaults,
{ imageAllowList: [], imageRegistry: "registry.example.com/paperclip" },
),
).toBe("registry.example.com/paperclip/agent-runtime-claude:v1");
});
it("accepts imageOverride when in allowlist", () => {
expect(
resolveImage(
{ imageOverride: "registry.example.com/mine:v2" },
defaults,
{ imageAllowList: ["registry.example.com/*:v2"], imageRegistry: undefined },
),
).toBe("registry.example.com/mine:v2");
});
it("rejects imageOverride not in allowlist", () => {
expect(() =>
resolveImage(
{ imageOverride: "evil.io/img:latest" },
defaults,
{ imageAllowList: ["registry.example.com/*"], imageRegistry: undefined },
),
).toThrow(/not in allowlist/);
});
});
@@ -0,0 +1,101 @@
import { describe, it, expect, vi } from "vitest";
import { createJob, deleteJob, getJobStatus, findPodForJob, JobTimeoutError, streamPodLogs, waitForJobCompletion } from "../../src/job-orchestrator.js";
describe("createJob", () => {
it("calls batch.createNamespacedJob with the manifest", async () => {
const create = vi.fn().mockResolvedValue({ metadata: { uid: "abc-uid" } });
const clients = { batch: { createNamespacedJob: create } };
const jobManifest = { apiVersion: "batch/v1", kind: "Job", metadata: { name: "r-1", namespace: "ns" }, spec: { template: {} } };
const result = await createJob(clients as never, "ns", jobManifest);
expect(create).toHaveBeenCalledWith({ namespace: "ns", body: jobManifest });
expect(result.uid).toBe("abc-uid");
});
});
describe("getJobStatus", () => {
it("returns phase=Succeeded when succeeded count is 1", async () => {
const get = vi.fn().mockResolvedValue({ status: { succeeded: 1, conditions: [{ type: "Complete", status: "True" }] } });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Succeeded");
expect(status.complete).toBe(true);
});
it("returns phase=Failed when failed count is >0", async () => {
const get = vi.fn().mockResolvedValue({ status: { failed: 1, conditions: [{ type: "Failed", status: "True", reason: "DeadlineExceeded" }] } });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Failed");
expect(status.reason).toBe("DeadlineExceeded");
});
it("returns phase=Running when active count is >0", async () => {
const get = vi.fn().mockResolvedValue({ status: { active: 1 } });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Running");
});
it("returns phase=Pending when no active/succeeded/failed counters set", async () => {
const get = vi.fn().mockResolvedValue({ status: {} });
const clients = { batch: { readNamespacedJobStatus: get } };
const status = await getJobStatus(clients as never, "ns", "r-1");
expect(status.phase).toBe("Pending");
});
});
describe("findPodForJob", () => {
it("lists pods by job-name label and returns the first running pod", async () => {
const list = vi.fn().mockResolvedValue({ items: [{ metadata: { name: "r-1-xyz" }, status: { phase: "Running" } }] });
const clients = { core: { listNamespacedPod: list } };
const podName = await findPodForJob(clients as never, "ns", "r-1");
expect(list).toHaveBeenCalledWith(expect.objectContaining({ namespace: "ns", labelSelector: "job-name=r-1" }));
expect(podName).toBe("r-1-xyz");
});
it("returns null when no pod is found", async () => {
const list = vi.fn().mockResolvedValue({ items: [] });
const clients = { core: { listNamespacedPod: list } };
const podName = await findPodForJob(clients as never, "ns", "r-1");
expect(podName).toBeNull();
});
});
describe("deleteJob", () => {
it("calls batch.deleteNamespacedJob with foreground propagation", async () => {
const del = vi.fn().mockResolvedValue({});
const clients = { batch: { deleteNamespacedJob: del } };
await deleteJob(clients as never, "ns", "r-1");
expect(del).toHaveBeenCalledWith(
expect.objectContaining({
namespace: "ns",
name: "r-1",
propagationPolicy: "Foreground",
}),
);
});
});
describe("streamPodLogs", () => {
it("emits pod log response bodies as stdout because Kubernetes pod logs are combined", async () => {
const readNamespacedPodLog = vi.fn().mockResolvedValue({ body: "hello\n" });
const clients = { core: { readNamespacedPodLog } };
const chunks: { stream: "stdout" | "stderr"; text: string }[] = [];
await streamPodLogs(clients as never, "ns", "pod-1", async (stream, text) => {
chunks.push({ stream, text });
});
expect(readNamespacedPodLog).toHaveBeenCalledWith({ namespace: "ns", name: "pod-1" });
expect(chunks).toEqual([{ stream: "stdout", text: "hello\n" }]);
});
});
describe("waitForJobCompletion", () => {
it("throws JobTimeoutError when the deadline is exceeded", async () => {
const get = vi.fn().mockResolvedValue({ status: { active: 1 } });
const clients = { batch: { readNamespacedJobStatus: get } };
await expect(
waitForJobCompletion(clients as never, "ns", "r-1", { timeoutMs: 50, pollMs: 10 }),
).rejects.toBeInstanceOf(JobTimeoutError);
});
});
@@ -0,0 +1,47 @@
import { describe, it, expect, vi } from "vitest";
import { KubeConfig } from "@kubernetes/client-node";
import { createKubeConfig } from "../../src/kube-client.js";
describe("createKubeConfig", () => {
it("loads from inline kubeconfig string", () => {
const yaml = `apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
server: https://fake.example.com
contexts:
- name: test
context:
cluster: test
user: test
current-context: test
users:
- name: test
user:
token: fake-token
`;
const kc = createKubeConfig({ inCluster: false, kubeconfig: yaml });
expect(kc.getCurrentContext()).toBe("test");
expect(kc.getCurrentCluster()?.server).toBe("https://fake.example.com");
});
it("loads from-cluster config when inCluster=true", () => {
const spy = vi.spyOn(KubeConfig.prototype, "loadFromCluster").mockImplementation(function (this: KubeConfig) {
this.loadFromString(`apiVersion: v1
kind: Config
clusters: [{name: in-cluster, cluster: {server: 'https://kubernetes.default.svc'}}]
contexts: [{name: in-cluster, context: {cluster: in-cluster, user: in-cluster}}]
current-context: in-cluster
users: [{name: in-cluster, user: {token: tok}}]`);
});
const kc = createKubeConfig({ inCluster: true });
expect(spy).toHaveBeenCalledOnce();
expect(kc.getCurrentContext()).toBe("in-cluster");
spy.mockRestore();
});
it("throws when neither inCluster nor kubeconfig string is provided", () => {
expect(() => createKubeConfig({ inCluster: false })).toThrow(/requires/i);
});
});
@@ -0,0 +1,65 @@
import { describe, it, expect } from "vitest";
import { buildNetworkPolicyManifests } from "../../src/network-policy.js";
describe("buildNetworkPolicyManifests", () => {
const baseInput = {
namespace: "paperclip-acme",
paperclipServerNamespace: "paperclip",
egressAllowFqdns: [] as string[],
egressAllowCidrs: [] as string[],
};
it("produces a deny-all + egress allow pair", () => {
const manifests = buildNetworkPolicyManifests(baseInput);
expect(manifests).toHaveLength(2);
expect(manifests[0].metadata.name).toBe("paperclip-deny-all");
expect(manifests[1].metadata.name).toBe("paperclip-egress-allow");
});
it("deny-all has no ingress/egress rules and applies to all pods", () => {
const [denyAll] = buildNetworkPolicyManifests(baseInput);
expect(denyAll.spec.podSelector).toEqual({});
expect(denyAll.spec.policyTypes).toEqual(["Ingress", "Egress"]);
expect(denyAll.spec.ingress).toBeUndefined();
expect(denyAll.spec.egress).toBeUndefined();
});
it("egress allow includes kube-dns and paperclip-server callback", () => {
const [, egress] = buildNetworkPolicyManifests(baseInput);
const rules = egress.spec.egress;
const dnsRule = rules.find((r: { ports?: { protocol: string; port: number }[] }) =>
r.ports?.some((p) => p.port === 53),
);
expect(dnsRule).toBeDefined();
const paperclipRule = rules.find((r: { to: { namespaceSelector?: { matchLabels?: Record<string, string> } }[] }) =>
r.to.some((t) => t.namespaceSelector?.matchLabels?.["kubernetes.io/metadata.name"] === "paperclip"),
);
expect(paperclipRule).toBeDefined();
});
it("includes user-supplied CIDRs in egress allow", () => {
const [, egress] = buildNetworkPolicyManifests({ ...baseInput, egressAllowCidrs: ["10.0.0.0/8"] });
const cidrRule = egress.spec.egress.find((r: { to: { ipBlock?: { cidr: string } }[] }) =>
r.to.some((t) => t.ipBlock?.cidr === "10.0.0.0/8"),
);
expect(cidrRule).toBeDefined();
});
it("adds a public HTTPS fallback when standard mode receives FQDN allow-list entries", () => {
const [, egress] = buildNetworkPolicyManifests({ ...baseInput, egressAllowFqdns: ["api.anthropic.com"] });
const publicHttpsRule = egress.spec.egress.find((r: { to: { ipBlock?: { cidr: string; except?: string[] } }[]; ports?: { port: number }[] }) =>
r.to.some((t) => t.ipBlock?.cidr === "0.0.0.0/0") && r.ports?.some((p) => p.port === 443),
);
expect(publicHttpsRule).toBeDefined();
expect(publicHttpsRule.to[0].ipBlock.except).toContain("10.0.0.0/8");
});
it("uses paperclip-server pod label selector for callback ingress to paperclip ns", () => {
const [, egress] = buildNetworkPolicyManifests(baseInput);
const callbackRule = egress.spec.egress.find((r: { to: { podSelector?: { matchLabels?: Record<string, string> } }[] }) =>
r.to.some((t) => t.podSelector?.matchLabels?.app === "paperclip-server"),
);
expect(callbackRule).toBeDefined();
expect(callbackRule.ports[0].port).toBe(3100);
});
});
@@ -0,0 +1,94 @@
import { describe, it, expect } from "vitest";
import plugin from "../../src/plugin.js";
describe("plugin", () => {
it("exports the kubernetes driver", () => {
expect(plugin.definition.onEnvironmentAcquireLease).toBeTypeOf("function");
expect(plugin.definition.onEnvironmentValidateConfig).toBeTypeOf("function");
});
it("validateConfig accepts inCluster=true config", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: { inCluster: true },
});
expect(result.ok).toBe(true);
});
it("validateConfig rejects missing auth", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: {},
});
expect(result.ok).toBe(false);
expect(result.errors?.[0]).toMatch(/requires one of `inCluster`/);
});
it("validateConfig normalizes defaults", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: { inCluster: true },
});
expect(result.ok).toBe(true);
expect(result.normalizedConfig).toEqual(
expect.objectContaining({
namespacePrefix: "paperclip-",
egressMode: "standard",
jobTtlSecondsAfterFinished: 900,
podActivityDeadlineSec: 3600,
adapterType: "claude_local",
backend: "sandbox-cr", // new default
}),
);
});
it("validateConfig accepts backend=sandbox-cr explicitly", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: { inCluster: true, backend: "sandbox-cr" },
});
expect(result.ok).toBe(true);
expect(result.normalizedConfig?.backend).toBe("sandbox-cr");
});
it("validateConfig accepts backend=job (stable fallback)", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: { inCluster: true, backend: "job" },
});
expect(result.ok).toBe(true);
expect(result.normalizedConfig?.backend).toBe("job");
});
it("validateConfig rejects unknown backend value", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: { inCluster: true, backend: "kata-fc" },
});
expect(result.ok).toBe(false);
});
it("onHealth returns ok", async () => {
const result = await plugin.definition.onHealth!();
expect(result.status).toBe("ok");
});
it("validateConfig warns about FQDN limitation in standard mode", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: { inCluster: true, adapterType: "claude_local" },
});
expect(result.ok).toBe(true);
expect(result.warnings).toBeDefined();
expect(result.warnings?.some((w) => w.includes("api.anthropic.com"))).toBe(true);
});
it("validateConfig does NOT warn when egressMode is cilium", async () => {
const result = await plugin.definition.onEnvironmentValidateConfig!({
driverKey: "kubernetes",
config: { inCluster: true, adapterType: "claude_local", egressMode: "cilium" },
});
expect(result.ok).toBe(true);
expect(result.warnings).toBeUndefined();
});
});
@@ -0,0 +1,95 @@
import { describe, it, expect } from "vitest";
import { buildJobManifest } from "../../src/pod-spec-builder.js";
const baseInput = {
namespace: "paperclip-acme",
jobName: "r-01h00000000000000000000000",
adapterType: "claude_local",
image: "ghcr.io/paperclipai/agent-runtime-claude:v1",
envSecretName: "r-01h00000000000000000000000-env",
serviceAccountName: "paperclip-tenant-sa",
labels: { "paperclip.io/run-id": "r1" },
resources: { requests: { cpu: "250m", memory: "512Mi" }, limits: { cpu: "2", memory: "4Gi" } },
runtimeClassName: undefined,
activeDeadlineSec: 3600,
ttlSecondsAfterFinished: 900,
};
describe("buildJobManifest", () => {
it("returns a Job manifest with the correct apiVersion and kind", () => {
const job = buildJobManifest(baseInput);
expect(job.apiVersion).toBe("batch/v1");
expect(job.kind).toBe("Job");
});
it("sets Job-level lifecycle controls: backoffLimit=0, ttlSecondsAfterFinished, activeDeadlineSeconds", () => {
const job = buildJobManifest({ ...baseInput, activeDeadlineSec: 1800, ttlSecondsAfterFinished: 600 });
expect(job.spec.backoffLimit).toBe(0);
expect(job.spec.ttlSecondsAfterFinished).toBe(600);
expect(job.spec.activeDeadlineSeconds).toBe(1800);
});
it("sets the security context to non-root, drop ALL caps, read-only rootFS, seccomp RuntimeDefault", () => {
const job = buildJobManifest(baseInput);
const podSec = job.spec.template.spec.securityContext;
expect(podSec.runAsNonRoot).toBe(true);
expect(podSec.runAsUser).toBe(1000);
expect(podSec.fsGroupChangePolicy).toBe("OnRootMismatch");
expect(podSec.seccompProfile.type).toBe("RuntimeDefault");
const container = job.spec.template.spec.containers[0];
expect(container.securityContext.runAsNonRoot).toBe(true);
expect(container.securityContext.readOnlyRootFilesystem).toBe(true);
expect(container.securityContext.allowPrivilegeEscalation).toBe(false);
expect(container.securityContext.capabilities.drop).toEqual(["ALL"]);
});
it("wraps the entrypoint in tini for PID 1", () => {
const job = buildJobManifest(baseInput);
const container = job.spec.template.spec.containers[0];
expect(container.command).toEqual(["/usr/bin/tini", "--", "/usr/local/bin/paperclip-agent-shim"]);
});
it("declares explicit writable emptyDir mounts for the standard agent paths", () => {
const job = buildJobManifest(baseInput);
const mounts = job.spec.template.spec.containers[0].volumeMounts;
const mountPaths = mounts.map((m: { mountPath: string }) => m.mountPath).sort();
expect(mountPaths).toEqual(["/home/paperclip", "/home/paperclip/.cache", "/tmp", "/workspace"]);
const volumes = job.spec.template.spec.volumes;
expect(volumes.every((v: { emptyDir?: unknown }) => v.emptyDir !== undefined)).toBe(true);
});
it("envFrom references the per-run secret", () => {
const job = buildJobManifest(baseInput);
const envFrom = job.spec.template.spec.containers[0].envFrom;
expect(envFrom[0].secretRef.name).toBe(baseInput.envSecretName);
});
it("applies runtimeClassName when set", () => {
const job = buildJobManifest({ ...baseInput, runtimeClassName: "kata-fc" });
expect(job.spec.template.spec.runtimeClassName).toBe("kata-fc");
});
it("does not set runtimeClassName when unset", () => {
const job = buildJobManifest(baseInput);
expect(job.spec.template.spec.runtimeClassName).toBeUndefined();
});
it("sets pod restartPolicy=Never (required for Job)", () => {
const job = buildJobManifest(baseInput);
expect(job.spec.template.spec.restartPolicy).toBe("Never");
});
it("disables automountServiceAccountToken to avoid exposing an unnecessary SA token", () => {
const job = buildJobManifest(baseInput);
expect(job.spec.template.spec.automountServiceAccountToken).toBe(false);
});
it("applies the provided labels to both Job metadata and pod template", () => {
const job = buildJobManifest(baseInput);
expect(job.metadata.labels["paperclip.io/run-id"]).toBe("r1");
expect(job.spec.template.metadata.labels["paperclip.io/run-id"]).toBe("r1");
expect(job.spec.template.metadata.labels["paperclip.io/role"]).toBe("agent");
});
});
@@ -0,0 +1,137 @@
import { describe, it, expect } from "vitest";
import { buildSandboxCrManifest } from "../../src/sandbox-cr-builder.js";
const baseInput = {
namespace: "paperclip-acme",
sandboxName: "pc-01h00000000000000000000000",
adapterType: "claude_local",
image: "ghcr.io/paperclipai/agent-runtime-claude:v1",
envSecretName: "pc-01h00000000000000000000000-env",
serviceAccountName: "paperclip-tenant-sa",
labels: { "paperclip.io/run-id": "r1" },
resources: {
requests: { cpu: "250m", memory: "512Mi" },
limits: { cpu: "2", memory: "4Gi" },
},
runtimeClassName: undefined,
};
describe("buildSandboxCrManifest", () => {
it("returns a Sandbox CR with the correct apiVersion and kind", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.apiVersion).toBe("agents.x-k8s.io/v1alpha1");
expect(cr.kind).toBe("Sandbox");
});
it("sets metadata name and namespace correctly", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.metadata.name).toBe(baseInput.sandboxName);
expect(cr.metadata.namespace).toBe(baseInput.namespace);
});
it("does NOT set ownerReferences (out-of-cluster server, explicit release path)", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.metadata.ownerReferences).toBeUndefined();
});
it("sets restartPolicy=Always on the pod template (required for long-lived Sandbox pod)", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.spec.podTemplate.spec.restartPolicy).toBe("Always");
});
it("uses sleep-infinity entrypoint via Tini for multi-command exec", () => {
const cr = buildSandboxCrManifest(baseInput);
const container = cr.spec.podTemplate.spec.containers[0];
expect(container.command).toEqual([
"/usr/bin/tini",
"--",
"/bin/sh",
"-c",
"sleep infinity",
]);
});
it("applies the same security baseline as Job backend (non-root, drop ALL, RO rootFS, seccomp)", () => {
const cr = buildSandboxCrManifest(baseInput);
const podSec = cr.spec.podTemplate.spec.securityContext;
expect(podSec.runAsNonRoot).toBe(true);
expect(podSec.runAsUser).toBe(1000);
expect(podSec.fsGroupChangePolicy).toBe("OnRootMismatch");
expect(podSec.seccompProfile.type).toBe("RuntimeDefault");
const container = cr.spec.podTemplate.spec.containers[0];
expect(container.securityContext.runAsNonRoot).toBe(true);
expect(container.securityContext.readOnlyRootFilesystem).toBe(true);
expect(container.securityContext.allowPrivilegeEscalation).toBe(false);
expect(container.securityContext.capabilities.drop).toEqual(["ALL"]);
});
it("disables automountServiceAccountToken", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.spec.podTemplate.spec.automountServiceAccountToken).toBe(false);
});
it("declares emptyDir volume mounts for standard agent paths", () => {
const cr = buildSandboxCrManifest(baseInput);
const mounts = cr.spec.podTemplate.spec.containers[0].volumeMounts;
const mountPaths = mounts
.map((m: { mountPath: string }) => m.mountPath)
.sort();
expect(mountPaths).toEqual([
"/home/paperclip",
"/home/paperclip/.cache",
"/tmp",
"/workspace",
]);
const volumes = cr.spec.podTemplate.spec.volumes;
expect(
volumes.every((v: { emptyDir?: unknown }) => v.emptyDir !== undefined),
).toBe(true);
});
it("envFrom references the per-run secret", () => {
const cr = buildSandboxCrManifest(baseInput);
const envFrom = cr.spec.podTemplate.spec.containers[0].envFrom;
expect(envFrom[0].secretRef.name).toBe(baseInput.envSecretName);
});
it("applies runtimeClassName when set", () => {
const cr = buildSandboxCrManifest({
...baseInput,
runtimeClassName: "kata-fc",
});
expect(cr.spec.podTemplate.spec.runtimeClassName).toBe("kata-fc");
});
it("does not set runtimeClassName when unset", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.spec.podTemplate.spec.runtimeClassName).toBeUndefined();
});
it("applies provided labels to CR metadata and pod template labels (with role=agent added)", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.metadata.labels["paperclip.io/run-id"]).toBe("r1");
expect(
cr.spec.podTemplate.metadata.labels["paperclip.io/run-id"],
).toBe("r1");
expect(cr.spec.podTemplate.metadata.labels["paperclip.io/role"]).toBe(
"agent",
);
});
it("applies imagePullSecrets when provided", () => {
const cr = buildSandboxCrManifest({
...baseInput,
imagePullSecrets: ["my-pull-secret"],
});
expect(cr.spec.podTemplate.spec.imagePullSecrets).toEqual([
{ name: "my-pull-secret" },
]);
});
it("does not set imagePullSecrets when not provided", () => {
const cr = buildSandboxCrManifest(baseInput);
expect(cr.spec.podTemplate.spec.imagePullSecrets).toBeUndefined();
});
});
@@ -0,0 +1,216 @@
import { describe, it, expect, vi } from "vitest";
import {
createSandboxCr,
deleteSandboxCr,
getSandboxCrStatus,
findPodForSandbox,
SandboxCrTimeoutError,
waitForSandboxReady,
} from "../../src/sandbox-cr-orchestrator.js";
const SANDBOX_GROUP = "agents.x-k8s.io";
const SANDBOX_VERSION = "v1alpha1";
const SANDBOX_PLURAL = "sandboxes";
// Helpers to build mock CR objects with given phase
function makeCr(phase: string, podName?: string): Record<string, unknown> {
return {
metadata: { uid: "sandbox-uid-123" },
status: {
phase,
...(podName ? { podName } : {}),
},
};
}
describe("createSandboxCr", () => {
it("calls custom.createNamespacedCustomObject with the correct params", async () => {
const create = vi.fn().mockResolvedValue({ metadata: { uid: "test-uid" } });
const clients = { custom: { createNamespacedCustomObject: create } };
const manifest = {
apiVersion: "agents.x-k8s.io/v1alpha1",
kind: "Sandbox",
metadata: { name: "pc-abc", namespace: "paperclip-acme" },
};
const result = await createSandboxCr(clients as never, "paperclip-acme", manifest);
expect(create).toHaveBeenCalledWith({
group: SANDBOX_GROUP,
version: SANDBOX_VERSION,
namespace: "paperclip-acme",
plural: SANDBOX_PLURAL,
body: manifest,
});
expect(result.uid).toBe("test-uid");
});
it("throws if the API response has no UID", async () => {
const create = vi.fn().mockResolvedValue({ metadata: {} });
const clients = { custom: { createNamespacedCustomObject: create } };
await expect(
createSandboxCr(clients as never, "ns", {}),
).rejects.toThrow("Sandbox CR created without a UID");
});
});
describe("getSandboxCrStatus", () => {
it("maps phase=Ready to SandboxStatus.phase=Running with active=1", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Ready"));
const clients = { custom: { getNamespacedCustomObject: get } };
const status = await getSandboxCrStatus(clients as never, "ns", "pc-abc");
expect(status.phase).toBe("Running");
expect(status.active).toBe(1);
expect(status.complete).toBe(false);
});
it("maps phase=Pending to SandboxStatus.phase=Pending", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Pending"));
const clients = { custom: { getNamespacedCustomObject: get } };
const status = await getSandboxCrStatus(clients as never, "ns", "pc-abc");
expect(status.phase).toBe("Pending");
expect(status.active).toBe(0);
});
it("maps phase=Failed to SandboxStatus.phase=Failed with failed=1", async () => {
const get = vi.fn().mockResolvedValue({
metadata: { uid: "uid-1" },
status: {
phase: "Failed",
conditions: [
{ type: "Failed", reason: "ImagePullFailed", message: "no image" },
],
},
});
const clients = { custom: { getNamespacedCustomObject: get } };
const status = await getSandboxCrStatus(clients as never, "ns", "pc-abc");
expect(status.phase).toBe("Failed");
expect(status.failed).toBe(1);
expect(status.reason).toBe("ImagePullFailed");
});
it("maps phase=Terminating to SandboxStatus.phase=Running with reason=Terminating", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Terminating"));
const clients = { custom: { getNamespacedCustomObject: get } };
const status = await getSandboxCrStatus(clients as never, "ns", "pc-abc");
expect(status.phase).toBe("Running");
expect(status.reason).toBe("Terminating");
});
});
describe("findPodForSandbox", () => {
it("returns status.podName from the Sandbox CR when set", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Ready", "pc-abc-pod-xyz"));
const clients = {
custom: { getNamespacedCustomObject: get },
core: { listNamespacedPod: vi.fn() },
};
const podName = await findPodForSandbox(clients as never, "ns", "pc-abc");
expect(podName).toBe("pc-abc-pod-xyz");
// Should NOT have called listNamespacedPod (primary path succeeded)
expect(clients.core.listNamespacedPod).not.toHaveBeenCalled();
});
it("falls back to pod listing when status.podName is absent", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Pending")); // no podName
const list = vi.fn().mockResolvedValue({
items: [
{
metadata: { name: "pc-abc-001", labels: { "paperclip.io/managed-by": "paperclip-k8s-plugin" } },
status: { phase: "Running" },
},
],
});
const clients = {
custom: { getNamespacedCustomObject: get },
core: { listNamespacedPod: list },
};
const podName = await findPodForSandbox(clients as never, "ns", "pc-abc");
// name starts with "pc-abc" → matched by prefix heuristic
expect(podName).toBe("pc-abc-001");
});
it("returns null when no pod is found in fallback", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Pending"));
const list = vi.fn().mockResolvedValue({ items: [] });
const clients = {
custom: { getNamespacedCustomObject: get },
core: { listNamespacedPod: list },
};
const podName = await findPodForSandbox(clients as never, "ns", "pc-abc");
expect(podName).toBeNull();
});
});
describe("deleteSandboxCr", () => {
it("calls custom.deleteNamespacedCustomObject with Foreground propagation", async () => {
const del = vi.fn().mockResolvedValue({});
const clients = { custom: { deleteNamespacedCustomObject: del } };
await deleteSandboxCr(clients as never, "ns", "pc-abc");
expect(del).toHaveBeenCalledWith(
expect.objectContaining({
group: SANDBOX_GROUP,
version: SANDBOX_VERSION,
namespace: "ns",
plural: SANDBOX_PLURAL,
name: "pc-abc",
propagationPolicy: "Foreground",
}),
);
});
});
describe("waitForSandboxReady", () => {
it("resolves immediately when Sandbox is already Ready", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Ready"));
const clients = { custom: { getNamespacedCustomObject: get } };
const status = await waitForSandboxReady(
clients as never,
"ns",
"pc-abc",
{ timeoutMs: 5000, pollMs: 10 },
);
expect(status.phase).toBe("Running"); // Ready maps to Running
expect(get).toHaveBeenCalledTimes(1);
});
it("polls until Ready", async () => {
const get = vi
.fn()
.mockResolvedValueOnce(makeCr("Pending"))
.mockResolvedValueOnce(makeCr("Pending"))
.mockResolvedValueOnce(makeCr("Ready"));
const clients = { custom: { getNamespacedCustomObject: get } };
const status = await waitForSandboxReady(
clients as never,
"ns",
"pc-abc",
{ timeoutMs: 5000, pollMs: 10 },
);
expect(status.phase).toBe("Running");
expect(get).toHaveBeenCalledTimes(3);
});
it("throws SandboxCrTimeoutError when deadline is exceeded", async () => {
const get = vi.fn().mockResolvedValue(makeCr("Pending"));
const clients = { custom: { getNamespacedCustomObject: get } };
await expect(
waitForSandboxReady(clients as never, "ns", "pc-abc", {
timeoutMs: 50,
pollMs: 10,
}),
).rejects.toBeInstanceOf(SandboxCrTimeoutError);
});
it("throws an error describing the failure when Sandbox fails", async () => {
const get = vi.fn().mockResolvedValue({
metadata: { uid: "u1" },
status: { phase: "Failed", conditions: [{ type: "Failed", reason: "OOMKilled" }] },
});
const clients = { custom: { getNamespacedCustomObject: get } };
await expect(
waitForSandboxReady(clients as never, "ns", "pc-abc", {
timeoutMs: 5000,
pollMs: 10,
}),
).rejects.toThrow(/failed.*OOMKilled/i);
});
});
@@ -0,0 +1,68 @@
import { describe, it, expect, vi } from "vitest";
import { createPerRunSecret } from "../../src/secret-manager.js";
describe("createPerRunSecret", () => {
const baseInput = {
namespace: "paperclip-acme",
secretName: "r-abcd-env",
runId: "r-abcd",
ownerKind: "Job",
ownerApiVersion: "batch/v1",
ownerName: "r-abcd",
ownerUid: "11111111-1111-1111-1111-111111111111",
bootstrapToken: "tok-xyz",
adapterEnv: { ANTHROPIC_API_KEY: "sk-test" },
};
it("creates a Secret with the correct name and namespace", async () => {
const created: { body: Record<string, unknown> }[] = [];
const clients = {
core: { createNamespacedSecret: vi.fn(async (args: { body: Record<string, unknown> }) => { created.push(args); }) },
};
await createPerRunSecret(clients as never, baseInput);
expect(clients.core.createNamespacedSecret).toHaveBeenCalledOnce();
const body = created[0].body as { metadata: { name: string; namespace: string } };
expect(body.metadata.name).toBe("r-abcd-env");
expect(body.metadata.namespace).toBe("paperclip-acme");
});
it("includes BOOTSTRAP_TOKEN and adapter env keys in stringData", async () => {
const created: { body: Record<string, unknown> }[] = [];
const clients = {
core: { createNamespacedSecret: vi.fn(async (args: { body: Record<string, unknown> }) => { created.push(args); }) },
};
await createPerRunSecret(clients as never, baseInput);
const body = created[0].body as { stringData: Record<string, string> };
expect(body.stringData.BOOTSTRAP_TOKEN).toBe("tok-xyz");
expect(body.stringData.ANTHROPIC_API_KEY).toBe("sk-test");
});
it("sets ownerReferences to the owner resource for cascade delete", async () => {
const created: { body: Record<string, unknown> }[] = [];
const clients = {
core: { createNamespacedSecret: vi.fn(async (args: { body: Record<string, unknown> }) => { created.push(args); }) },
};
await createPerRunSecret(clients as never, baseInput);
const body = created[0].body as { metadata: { ownerReferences: { uid: string; controller: boolean }[] } };
expect(body.metadata.ownerReferences).toHaveLength(1);
expect(body.metadata.ownerReferences[0].uid).toBe("11111111-1111-1111-1111-111111111111");
expect(body.metadata.ownerReferences[0].controller).toBe(true);
});
it("throws if adapterEnv contains BOOTSTRAP_TOKEN", async () => {
const clients = { core: { createNamespacedSecret: vi.fn() } };
await expect(
createPerRunSecret(clients as never, {
...baseInput,
adapterEnv: { BOOTSTRAP_TOKEN: "evil" },
}),
).rejects.toThrow(/BOOTSTRAP_TOKEN/);
});
it("throws if ownerUid is empty", async () => {
const clients = { core: { createNamespacedSecret: vi.fn() } };
await expect(
createPerRunSecret(clients as never, { ...baseInput, ownerUid: "" }),
).rejects.toThrow(/ownerUid/);
});
});
@@ -0,0 +1,153 @@
import { describe, it, expect, vi } from "vitest";
import { ensureTenant } from "../../src/tenant-orchestrator.js";
function makeMockClients() {
const calls: { kind: string; name: string; namespace?: string; body?: unknown }[] = [];
function track(kind: string) {
return vi.fn(async (...args: unknown[]) => {
const arg = (args[0] ?? {}) as { name?: string; namespace?: string; body?: unknown };
calls.push({ kind, name: arg.name ?? "", namespace: arg.namespace, body: arg.body });
return { body: arg.body };
});
}
return {
calls,
core: {
createNamespace: track("Namespace"),
readNamespacedServiceAccount: vi.fn().mockRejectedValue({ code: 404 }),
createNamespacedServiceAccount: track("ServiceAccount"),
replaceNamespacedServiceAccount: track("ServiceAccountReplace"),
readNamespacedResourceQuota: vi.fn().mockRejectedValue({ code: 404 }),
createNamespacedResourceQuota: track("ResourceQuota"),
replaceNamespacedResourceQuota: track("ResourceQuotaReplace"),
readNamespacedLimitRange: vi.fn().mockRejectedValue({ code: 404 }),
createNamespacedLimitRange: track("LimitRange"),
replaceNamespacedLimitRange: track("LimitRangeReplace"),
readNamespace: vi.fn().mockRejectedValue({ code: 404 }),
},
rbac: {
readNamespacedRole: vi.fn().mockRejectedValue({ code: 404 }),
createNamespacedRole: track("Role"),
replaceNamespacedRole: track("RoleReplace"),
readNamespacedRoleBinding: vi.fn().mockRejectedValue({ code: 404 }),
createNamespacedRoleBinding: track("RoleBinding"),
replaceNamespacedRoleBinding: track("RoleBindingReplace"),
},
networking: {
readNamespacedNetworkPolicy: vi.fn().mockRejectedValue({ code: 404 }),
createNamespacedNetworkPolicy: track("NetworkPolicy"),
replaceNamespacedNetworkPolicy: track("NetworkPolicyReplace"),
deleteNamespacedNetworkPolicy: vi.fn().mockRejectedValue({ code: 404 }),
},
custom: {
getNamespacedCustomObject: vi.fn().mockRejectedValue({ code: 404 }),
createNamespacedCustomObject: track("CiliumNetworkPolicy"),
replaceNamespacedCustomObject: track("CiliumNetworkPolicyReplace"),
deleteNamespacedCustomObject: vi.fn().mockRejectedValue({ code: 404 }),
},
};
}
describe("ensureTenant", () => {
const baseInput = {
namespace: "paperclip-acme",
companyId: "11111111-1111-1111-1111-111111111111",
paperclipServerNamespace: "paperclip",
serviceAccountAnnotations: {},
egressMode: "standard" as const,
egressAllowFqdns: ["api.anthropic.com"],
egressAllowCidrs: [] as string[],
resourceQuota: { pods: "20", requestsCpu: "5", requestsMemory: "20Gi", limitsCpu: "20", limitsMemory: "80Gi" },
};
it("creates all required resources in the correct order on a fresh tenant", async () => {
const clients = makeMockClients();
await ensureTenant(clients as never, baseInput);
const order = clients.calls.map((c) => c.kind);
expect(order).toEqual([
"Namespace",
"ServiceAccount",
"Role",
"RoleBinding",
"ResourceQuota",
"LimitRange",
"NetworkPolicy",
"NetworkPolicy",
]);
});
it("creates a CiliumNetworkPolicy instead of standard egress when egressMode=cilium", async () => {
const clients = makeMockClients();
await ensureTenant(clients as never, { ...baseInput, egressMode: "cilium" });
const cnpCall = clients.calls.find((c) => c.kind === "CiliumNetworkPolicy");
expect(cnpCall).toBeDefined();
const npCalls = clients.calls.filter((c) => c.kind === "NetworkPolicy");
expect(npCalls).toHaveLength(1);
expect((npCalls[0].body as { metadata: { name: string } }).metadata.name).toBe("paperclip-deny-all");
});
it("applies serviceAccountAnnotations to the ServiceAccount", async () => {
const clients = makeMockClients();
await ensureTenant(clients as never, {
...baseInput,
serviceAccountAnnotations: { "eks.amazonaws.com/role-arn": "arn:aws:iam::123:role/paperclip" },
});
const saCall = clients.calls.find((c) => c.kind === "ServiceAccount");
const sa = saCall!.body as { metadata: { annotations: Record<string, string> } };
expect(sa.metadata.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123:role/paperclip");
});
it("does not recreate a namespace that already exists", async () => {
const clients = makeMockClients();
clients.core.readNamespace.mockResolvedValue({ body: { metadata: { name: baseInput.namespace } } });
await ensureTenant(clients as never, baseInput);
expect(clients.core.createNamespace).not.toHaveBeenCalled();
});
it("reconciles existing managed resources with the latest desired manifests", async () => {
const clients = makeMockClients();
const existing = { metadata: { resourceVersion: "rv-1" } };
clients.core.readNamespace.mockResolvedValue({ metadata: { name: baseInput.namespace } });
clients.core.readNamespacedServiceAccount.mockResolvedValue(existing);
clients.rbac.readNamespacedRole.mockResolvedValue(existing);
clients.rbac.readNamespacedRoleBinding.mockResolvedValue(existing);
clients.core.readNamespacedResourceQuota.mockResolvedValue(existing);
clients.core.readNamespacedLimitRange.mockResolvedValue(existing);
clients.networking.readNamespacedNetworkPolicy.mockResolvedValue(existing);
await ensureTenant(clients as never, {
...baseInput,
serviceAccountAnnotations: { "eks.amazonaws.com/role-arn": "arn:aws:iam::123:role/paperclip" },
resourceQuota: { ...baseInput.resourceQuota, pods: "25" },
});
expect(clients.core.replaceNamespacedServiceAccount).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({
metadata: expect.objectContaining({
annotations: { "eks.amazonaws.com/role-arn": "arn:aws:iam::123:role/paperclip" },
resourceVersion: "rv-1",
}),
}),
}),
);
expect(clients.core.replaceNamespacedResourceQuota).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({
metadata: expect.objectContaining({ resourceVersion: "rv-1" }),
spec: expect.objectContaining({ hard: expect.objectContaining({ pods: "25" }) }),
}),
}),
);
expect(clients.networking.replaceNamespacedNetworkPolicy).toHaveBeenCalled();
});
it("removes stale standard egress NetworkPolicy when cilium mode is selected", async () => {
const clients = makeMockClients();
await ensureTenant(clients as never, { ...baseInput, egressMode: "cilium" });
expect(clients.networking.deleteNamespacedNetworkPolicy).toHaveBeenCalledWith({
namespace: baseInput.namespace,
name: "paperclip-egress-allow",
});
});
});
@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import { kubernetesProviderConfigSchema, parseKubernetesProviderConfig } from "../../src/types.js";
describe("kubernetesProviderConfigSchema", () => {
it("accepts inCluster=true with no kubeconfig", () => {
const parsed = parseKubernetesProviderConfig({ inCluster: true });
expect(parsed.inCluster).toBe(true);
expect(parsed.namespacePrefix).toBe("paperclip-");
expect(parsed.imageAllowList).toEqual([]);
expect(parsed.egressMode).toBe("standard");
expect(parsed.jobTtlSecondsAfterFinished).toBe(900);
});
it("accepts inline kubeconfig", () => {
const parsed = parseKubernetesProviderConfig({
inCluster: false,
kubeconfig: "apiVersion: v1\nkind: Config\n",
});
expect(parsed.kubeconfig).toContain("apiVersion");
});
it("rejects when neither inCluster nor any kubeconfig source is set", () => {
expect(() => parseKubernetesProviderConfig({ inCluster: false })).toThrow(
/requires one of `inCluster` or `kubeconfig`/,
);
});
it("rejects invalid companySlug", () => {
expect(() =>
parseKubernetesProviderConfig({ inCluster: true, companySlug: "INVALID UPPER" }),
).toThrow();
});
it("rejects egressAllowCidrs entries that are not valid CIDR", () => {
expect(() =>
parseKubernetesProviderConfig({ inCluster: true, egressAllowCidrs: ["not-a-cidr"] }),
).toThrow(/CIDR/i);
});
});
@@ -0,0 +1,42 @@
import { describe, it, expect } from "vitest";
import { deriveCompanySlug, deriveNamespaceName, newRunUlidDns, paperclipLabels } from "../../src/utils.js";
describe("deriveCompanySlug", () => {
it("lowercases and replaces non-alphanumerics", () => {
expect(deriveCompanySlug("Acme Co!")).toBe("acme-co");
});
it("truncates to 32 chars and strips trailing dashes", () => {
expect(deriveCompanySlug("A".repeat(50))).toBe("a".repeat(32));
expect(deriveCompanySlug("ab---")).toBe("ab");
});
it("falls back to 'company' on empty/zero-letter input", () => {
expect(deriveCompanySlug("!!!")).toBe("company");
expect(deriveCompanySlug("")).toBe("company");
});
});
describe("deriveNamespaceName", () => {
it("concatenates prefix and slug", () => {
expect(deriveNamespaceName("paperclip-", "acme-co")).toBe("paperclip-acme-co");
});
});
describe("newRunUlidDns", () => {
it("produces a DNS-safe 26-char lowercase id", () => {
const id = newRunUlidDns();
expect(id).toMatch(/^[a-z0-9]{26}$/);
});
});
describe("paperclipLabels", () => {
it("returns canonical label map", () => {
const labels = paperclipLabels({ runId: "r1", agentId: "a1", companyId: "c1", adapterType: "claude_local" });
expect(labels["paperclip.io/run-id"]).toBe("r1");
expect(labels["paperclip.io/agent-id"]).toBe("a1");
expect(labels["paperclip.io/company-id"]).toBe("c1");
expect(labels["paperclip.io/adapter"]).toBe("claude_local");
expect(labels["paperclip.io/managed-by"]).toBe("paperclip-k8s-plugin");
});
});