@app-vitals/agent-admin 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -239,7 +239,7 @@ test.describe("GET /admin/agents/:id — authenticated", () => {
239
239
  context,
240
240
  }) => {
241
241
  await loadAgentDetailPage(page, context);
242
- await expect(page.locator("body")).toContainText("coding");
242
+ await expect(page.locator("body")).toContainText("specialist");
243
243
  });
244
244
 
245
245
  // CFB-3.1: .data-table .btn must stay at its current ~36px height at mobile
@@ -304,16 +304,16 @@ test.describe("GET /admin/agents/new — type picker", () => {
304
304
  await expect(typeSelect).toHaveAttribute("required", "");
305
305
  });
306
306
 
307
- test('the select lists the configured default type "coding"', async ({
307
+ test('the select lists the configured default type "specialist"', async ({
308
308
  page,
309
309
  context,
310
310
  }) => {
311
311
  await loadNewAgentPage(page, context);
312
- const codingOption = page.locator(
313
- 'select[name="type"] option[value="coding"]',
312
+ const specialistOption = page.locator(
313
+ 'select[name="type"] option[value="specialist"]',
314
314
  );
315
- await expect(codingOption).toBeAttached();
316
- await expect(codingOption).toHaveText("coding");
315
+ await expect(specialistOption).toBeAttached();
316
+ await expect(specialistOption).toHaveText("Specialist Agent");
317
317
  });
318
318
  });
319
319
 
@@ -324,8 +324,8 @@ test.describe("POST /admin/agents — create-with-type journey", () => {
324
324
  }) => {
325
325
  await loadNewAgentPage(page, context);
326
326
 
327
- await page.fill('input[name="name"]', "My New Coding Agent");
328
- await page.selectOption('select[name="type"]', "coding");
327
+ await page.fill('input[name="name"]', "My New Specialist Agent");
328
+ await page.selectOption('select[name="type"]', "specialist");
329
329
  // Scoped to the new-agent form specifically — the page also has a
330
330
  // "Sign out" button[type="submit"] in the toolbar, and an unscoped
331
331
  // selector matches that one first (it's earlier in the DOM), which
@@ -333,6 +333,6 @@ test.describe("POST /admin/agents — create-with-type journey", () => {
333
333
  await page.click('form[action="/admin/agents"] button[type="submit"]');
334
334
 
335
335
  await page.waitForURL(/\/admin\/agents\/[^/]+$/);
336
- await expect(page.locator("body")).toContainText("coding");
336
+ await expect(page.locator("body")).toContainText("specialist");
337
337
  });
338
338
  });
@@ -30,13 +30,13 @@ import type {
30
30
  // ─── Config ───────────────────────────────────────────────────────────────────
31
31
 
32
32
  // The suite runs without a product specialty: point the specialty lookup at the
33
- // minimal "coding" fixture next to this file (values already set win).
33
+ // minimal "specialist" fixture next to this file (values already set win).
34
34
  process.env.SHIPWRIGHT_SPECIALTIES_DIR ||= resolve(
35
35
  import.meta.dir,
36
36
  "fixtures",
37
37
  "specialties",
38
38
  );
39
- process.env.SHIPWRIGHT_DEFAULT_AGENT_TYPE ||= "coding";
39
+ process.env.SHIPWRIGHT_DEFAULT_AGENT_TYPE ||= "specialist";
40
40
 
41
41
  const ADMIN_E2E_PORT = 3490;
42
42
  const ADMIN_E2E_SESSION_SECRET =
@@ -324,7 +324,7 @@ function buildMockDeps(chatClient: ChatClient | undefined): AdminUIDeps {
324
324
  name: ADMIN_E2E_AGENT.name,
325
325
  slackId: ADMIN_E2E_AGENT.slackId,
326
326
  selfHosted: false,
327
- specialty: "coding",
327
+ specialty: "specialist",
328
328
  createdAt: new Date("2024-01-01"),
329
329
  updatedAt: new Date("2024-01-01"),
330
330
  },
@@ -339,7 +339,7 @@ function buildMockDeps(chatClient: ChatClient | undefined): AdminUIDeps {
339
339
  name: input.name ?? ADMIN_E2E_AGENT.name,
340
340
  slackId: ADMIN_E2E_AGENT.slackId,
341
341
  selfHosted: false,
342
- specialty: input.specialty ?? "coding",
342
+ specialty: input.specialty ?? "specialist",
343
343
  createdAt: new Date("2024-01-01"),
344
344
  updatedAt: new Date("2024-01-01"),
345
345
  }),
@@ -349,7 +349,7 @@ function buildMockDeps(chatClient: ChatClient | undefined): AdminUIDeps {
349
349
  name: ADMIN_E2E_AGENT.name,
350
350
  slackId: ADMIN_E2E_AGENT.slackId,
351
351
  selfHosted: false,
352
- specialty: "coding",
352
+ specialty: "specialist",
353
353
  createdAt: new Date("2024-01-01"),
354
354
  updatedAt: new Date("2024-01-01"),
355
355
  repos: [],
@@ -361,7 +361,7 @@ function buildMockDeps(chatClient: ChatClient | undefined): AdminUIDeps {
361
361
  name: ADMIN_E2E_AGENT.name,
362
362
  slackId: ADMIN_E2E_AGENT.slackId,
363
363
  selfHosted: false,
364
- specialty: "coding",
364
+ specialty: "specialist",
365
365
  createdAt: new Date("2024-01-01"),
366
366
  updatedAt: new Date("2024-01-01"),
367
367
  repos: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-vitals/agent-admin",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/app-vitals/agent.git",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "0.135.0",
45
- "@app-vitals/agent-lib": "1.0.0",
45
+ "@app-vitals/agent-lib": "1.2.0",
46
46
  "@hono/zod-openapi": "^1.0.0",
47
47
  "@prisma/adapter-pg": "^7.10.0",
48
48
  "@prisma/client": "^7.10.0",
@@ -12,6 +12,7 @@
12
12
  import { join } from "node:path";
13
13
  import type { PrismaClient } from "../prisma/client/client.ts";
14
14
  import { AgentAnthropicIdentityService } from "./agent-anthropic-identity.ts";
15
+ import { parseAgentPodLabels } from "./agent-pod-labels.ts";
15
16
  import { parseAgentImageMap } from "./agent-image-map.ts";
16
17
  import type { AgentAnthropicFederation } from "./agent-manifest.ts";
17
18
  import type {
@@ -123,6 +124,7 @@ export function buildProvisioner(
123
124
  const imageTag = env.SHIPWRIGHT_AGENT_IMAGE_TAG ?? "latest";
124
125
  // Malformed → throws here, so admin boot fails rather than falling back.
125
126
  const images = parseAgentImageMap(env.SHIPWRIGHT_AGENT_IMAGES);
127
+ const podLabels = parseAgentPodLabels(env.SHIPWRIGHT_K8S_AGENT_POD_LABELS);
126
128
  const apiUrl = env.SHIPWRIGHT_API_URL ?? "";
127
129
  const replicasRaw = env.SHIPWRIGHT_AGENT_REPLICAS;
128
130
  const replicas = replicasRaw ? Number(replicasRaw) : undefined;
@@ -205,6 +207,7 @@ export function buildProvisioner(
205
207
  image,
206
208
  imageTag,
207
209
  ...(images ? { images } : {}),
210
+ ...(podLabels ? { podLabels } : {}),
208
211
  apiUrl,
209
212
  ...(replicas !== undefined && Number.isFinite(replicas)
210
213
  ? { replicas }
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { buildDeletionClients } from "./admin-boot-config.ts";
2
+ import type { AgentTokenService } from "./agent-tokens.ts";
3
+ import { buildProvisioner, buildDeletionClients } from "./admin-boot-config.ts";
3
4
  import {
4
5
  HttpChatServiceProvisioningClient,
5
6
  NoopChatServiceProvisioningClient,
@@ -26,3 +27,17 @@ describe("buildDeletionClients", () => {
26
27
  expect(chatService).toBeInstanceOf(HttpChatServiceProvisioningClient);
27
28
  });
28
29
  });
30
+
31
+ describe("buildProvisioner pod label configuration", () => {
32
+ it("rejects invalid labels during admin boot when provisioning is enabled", () => {
33
+ expect(() =>
34
+ buildProvisioner(
35
+ {
36
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
37
+ SHIPWRIGHT_K8S_AGENT_POD_LABELS: '{"app.kubernetes.io/name":"wrong"}',
38
+ },
39
+ {} as AgentTokenService,
40
+ ),
41
+ ).toThrow("reserved key");
42
+ });
43
+ });
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { createHash } from "node:crypto";
15
+ import { validateAgentPodLabels } from "./agent-pod-labels.ts";
15
16
  import type {
16
17
  KubernetesDeployment,
17
18
  KubernetesEnvVar,
@@ -239,6 +240,8 @@ export function buildAgentPvcManifest(opts: AgentPvcOpts): KubernetesPvc {
239
240
  // ─── Deployment builder ─────────────────────────────────────────────────────
240
241
 
241
242
  export interface AgentDeploymentOpts {
243
+ /** Additional pod labels; reserved identity labels cannot be overridden. */
244
+ podLabels?: Record<string, string>;
242
245
  /** Original agent id (used verbatim in env + label, sanitized for the name). */
243
246
  agentId: string;
244
247
  namespace: string;
@@ -471,6 +474,7 @@ export function buildAgentDeploymentManifest(
471
474
  opts: AgentDeploymentOpts,
472
475
  ): KubernetesDeployment {
473
476
  const name = sanitizeAgentName(opts.agentId);
477
+ const podLabels = validateAgentPodLabels(opts.podLabels ?? {});
474
478
  const tokenKey = opts.tokenSecretKey ?? "token";
475
479
  const volumeName = "agent-home";
476
480
 
@@ -513,9 +517,11 @@ export function buildAgentDeploymentManifest(
513
517
  selector: { matchLabels: selectorLabels },
514
518
  template: {
515
519
  metadata: {
516
- labels: tenant
517
- ? { ...selectorLabels, [TENANT_LABEL]: "true" }
518
- : selectorLabels,
520
+ labels: {
521
+ ...selectorLabels,
522
+ ...podLabels,
523
+ ...(tenant ? { [TENANT_LABEL]: "true" } : {}),
524
+ },
519
525
  ...(policy?.hash
520
526
  ? { annotations: { [CLAUDE_POLICY_HASH_ANNOTATION]: policy.hash } }
521
527
  : {}),
@@ -75,6 +75,35 @@ describe("buildAgentDeploymentManifest", () => {
75
75
  });
76
76
  });
77
77
 
78
+ it("adds configured pod labels without changing immutable selectors or identity labels", () => {
79
+ const base = buildAgentDeploymentManifest(deployOpts);
80
+ const podLabels = {
81
+ "vgs.io/service": "shipwright",
82
+ "vgs.io/team": "team-infrastructure",
83
+ };
84
+ const labeled = buildAgentDeploymentManifest({
85
+ ...deployOpts,
86
+ podLabels,
87
+ accountId: "account-1",
88
+ });
89
+ expect(labeled.spec.selector).toEqual(base.spec.selector);
90
+ expect(labeled.metadata).toEqual({
91
+ ...base.metadata,
92
+ labels: { ...base.metadata.labels, [TENANT_LABEL]: "true" },
93
+ });
94
+ expect(labeled.spec.template.metadata.labels).toEqual({
95
+ ...base.spec.template.metadata.labels,
96
+ ...podLabels,
97
+ [TENANT_LABEL]: "true",
98
+ });
99
+ expect(() =>
100
+ buildAgentDeploymentManifest({
101
+ ...deployOpts,
102
+ podLabels: { "app.kubernetes.io/instance": "other" },
103
+ }),
104
+ ).toThrow();
105
+ });
106
+
78
107
  it("sets the required env vars including a secretKeyRef token", () => {
79
108
  const d = buildAgentDeploymentManifest(deployOpts);
80
109
  const env = d.spec.template.spec.containers[0].env ?? [];
@@ -0,0 +1,53 @@
1
+ /** Optional labels for agent pod templates; identity selectors stay platform-owned. */
2
+ const RESERVED = new Set([
3
+ "app.kubernetes.io/name",
4
+ "app.kubernetes.io/instance",
5
+ "app.kubernetes.io/managed-by",
6
+ "shipwright.dev/agent-id",
7
+ "shipwright.dev/tenant",
8
+ ]);
9
+ const NAME = /^[A-Za-z0-9]([A-Za-z0-9_.-]*[A-Za-z0-9])?$/;
10
+ const DNS_LABEL = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
11
+
12
+ export function validateAgentPodLabels(value: unknown): Record<string, string> {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
14
+ throw new Error("SHIPWRIGHT_K8S_AGENT_POD_LABELS must be a JSON object");
15
+ }
16
+ for (const [key, label] of Object.entries(value)) {
17
+ const parts = key.split("/");
18
+ const name = parts.at(-1) ?? "";
19
+ const prefix = parts.length === 2 ? parts[0] : undefined;
20
+ if (
21
+ parts.length > 2 ||
22
+ name.length > 63 ||
23
+ !NAME.test(name) ||
24
+ (prefix !== undefined &&
25
+ (prefix.length > 253 ||
26
+ prefix
27
+ .split(".")
28
+ .some((part) => part.length > 63 || !DNS_LABEL.test(part)))) ||
29
+ RESERVED.has(key)
30
+ ) {
31
+ throw new Error(
32
+ `SHIPWRIGHT_K8S_AGENT_POD_LABELS has an invalid or reserved key: ${key}`,
33
+ );
34
+ }
35
+ if (
36
+ typeof label !== "string" ||
37
+ label.length > 63 ||
38
+ (label !== "" && !NAME.test(label))
39
+ ) {
40
+ throw new Error(
41
+ `SHIPWRIGHT_K8S_AGENT_POD_LABELS has an invalid value for ${key}`,
42
+ );
43
+ }
44
+ }
45
+ return { ...value } as Record<string, string>;
46
+ }
47
+
48
+ export function parseAgentPodLabels(
49
+ raw: string | undefined,
50
+ ): Record<string, string> | undefined {
51
+ if (raw === undefined || raw.trim() === "") return undefined;
52
+ return validateAgentPodLabels(JSON.parse(raw));
53
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ parseAgentPodLabels,
4
+ validateAgentPodLabels,
5
+ } from "./agent-pod-labels.ts";
6
+
7
+ describe("agent pod label configuration", () => {
8
+ it("leaves unconfigured labels absent", () => {
9
+ expect(parseAgentPodLabels(undefined)).toBeUndefined();
10
+ expect(parseAgentPodLabels(" ")).toBeUndefined();
11
+ expect(parseAgentPodLabels("{}")).toEqual({});
12
+ });
13
+ it("accepts qualified keys, plain keys and Kubernetes label values", () => {
14
+ expect(
15
+ parseAgentPodLabels(
16
+ '{"vgs.io/team":"team-infrastructure","app":"shipwright","empty":""}',
17
+ ),
18
+ ).toEqual({
19
+ "vgs.io/team": "team-infrastructure",
20
+ app: "shipwright",
21
+ empty: "",
22
+ });
23
+ });
24
+ for (const raw of [
25
+ "null",
26
+ "[]",
27
+ "false",
28
+ "1",
29
+ "invalid",
30
+ '{"app":1}',
31
+ '{"app":null}',
32
+ ]) {
33
+ it(`rejects malformed config ${raw}`, () =>
34
+ expect(() => parseAgentPodLabels(raw)).toThrow());
35
+ }
36
+ for (const key of [
37
+ "",
38
+ "/app",
39
+ "UPPER.io/app",
40
+ "bad..io/app",
41
+ "a/b/c",
42
+ "a".repeat(64),
43
+ "app.kubernetes.io/name",
44
+ "app.kubernetes.io/instance",
45
+ "app.kubernetes.io/managed-by",
46
+ "shipwright.dev/agent-id",
47
+ "shipwright.dev/tenant",
48
+ ]) {
49
+ it(`rejects invalid or reserved key ${key}`, () =>
50
+ expect(() => validateAgentPodLabels({ [key]: "x" })).toThrow());
51
+ }
52
+ for (const value of [
53
+ "bad/value",
54
+ "bad value",
55
+ "-bad",
56
+ "bad-",
57
+ "x".repeat(64),
58
+ ]) {
59
+ it(`rejects invalid value ${value}`, () =>
60
+ expect(() => validateAgentPodLabels({ app: value })).toThrow());
61
+ }
62
+ });
@@ -239,6 +239,8 @@ export interface AgentProvisioner {
239
239
  // ─── Configuration ──────────────────────────────────────────────────────────
240
240
 
241
241
  export interface KubernetesAgentProvisionerConfig {
242
+ /** Additional labels managed on new and existing agent pod templates. */
243
+ podLabels?: Record<string, string>;
242
244
  /** Namespace the agent resources live in. */
243
245
  namespace: string;
244
246
  /** Agent container image (without tag). */
@@ -940,6 +942,7 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
940
942
  tokenSecretKey: this.tokenKey,
941
943
  replicas: this.config.replicas,
942
944
  resources: this.config.resources,
945
+ podLabels: this.config.podLabels,
943
946
  voice: this.config.voice,
944
947
  serviceIntegrations: provisionableIntegrations(
945
948
  this.config.serviceIntegrations,
@@ -1486,11 +1489,18 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
1486
1489
  );
1487
1490
  const desired = desiredDeployment.spec.template.spec.containers[0];
1488
1491
  const desiredLabels = desiredDeployment.spec.template.metadata.labels;
1489
- const labelsDrifted =
1490
- !policyOnly &&
1491
- desiredLabels[TENANT_LABEL] !== undefined &&
1492
- deployment.spec.template.metadata?.labels?.[TENANT_LABEL] !==
1493
- desiredLabels[TENANT_LABEL];
1492
+ // Manage only configured keys; preserve labels owned by other actors.
1493
+ // Startup policy reconciliation also propagates ownership labels.
1494
+ const managedLabels = {
1495
+ ...(this.config.podLabels ?? {}),
1496
+ ...(!policyOnly && desiredLabels[TENANT_LABEL] !== undefined
1497
+ ? { [TENANT_LABEL]: desiredLabels[TENANT_LABEL] }
1498
+ : {}),
1499
+ };
1500
+ const labelsDrifted = Object.entries(managedLabels).some(
1501
+ ([key, value]) =>
1502
+ deployment.spec.template.metadata?.labels?.[key] !== value,
1503
+ );
1494
1504
  const policy = policyPatch(deployment, desiredDeployment);
1495
1505
  // Unmanaged federation (see reconcileFederationFor): no patch, so
1496
1506
  // the live ServiceAccount, token volume and env are left alone.
@@ -1571,9 +1581,7 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
1571
1581
  }
1572
1582
  : {};
1573
1583
  const podMetadata = {
1574
- ...(labelsDrifted
1575
- ? { labels: { [TENANT_LABEL]: desiredLabels[TENANT_LABEL] } }
1576
- : {}),
1584
+ ...(labelsDrifted ? { labels: managedLabels } : {}),
1577
1585
  ...(policy?.annotations
1578
1586
  ? { annotations: policy.annotations }
1579
1587
  : {}),
@@ -1583,7 +1591,9 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
1583
1591
  this.config.namespace,
1584
1592
  resourceName,
1585
1593
  {
1586
- ...(labelsDrifted
1594
+ ...(labelsDrifted &&
1595
+ !policyOnly &&
1596
+ desiredLabels[TENANT_LABEL] !== undefined
1587
1597
  ? {
1588
1598
  metadata: {
1589
1599
  labels: {
@@ -2034,3 +2034,75 @@ describe("KubernetesAgentProvisioner.reconcile() — pod ServiceAccount drift (C
2034
2034
  expect(patches).toHaveLength(0);
2035
2035
  });
2036
2036
  });
2037
+
2038
+ describe("KubernetesAgentProvisioner configured pod labels", () => {
2039
+ const agentId = "agent-label-example";
2040
+ const podLabels = {
2041
+ "vgs.io/service": "shipwright",
2042
+ "vgs.io/team": "team-infrastructure",
2043
+ };
2044
+ for (const policyOnly of [false, true]) {
2045
+ it(`updates existing templates and preserves selectors/unrelated labels (policyOnly=${policyOnly})`, async () => {
2046
+ const client = emptyClient();
2047
+ const original = new KubernetesAgentProvisioner(
2048
+ client,
2049
+ stubTokens() as AgentTokenService,
2050
+ BASE_CONFIG,
2051
+ );
2052
+ await original.provision(agentId);
2053
+ const name = sanitizeAgentName(agentId);
2054
+ const before = await client.getDeployment(NAMESPACE, name);
2055
+ const selector = structuredClone(before.spec.selector);
2056
+ await client.patchDeployment(NAMESPACE, name, {
2057
+ spec: {
2058
+ template: {
2059
+ metadata: {
2060
+ labels: {
2061
+ "other.io/label": "preserved",
2062
+ "vgs.io/team": "NOT_SET",
2063
+ },
2064
+ },
2065
+ },
2066
+ },
2067
+ });
2068
+ const next = new KubernetesAgentProvisioner(
2069
+ client,
2070
+ stubTokens() as AgentTokenService,
2071
+ { ...BASE_CONFIG, podLabels },
2072
+ );
2073
+ const preview = await next.reconcile([{ id: agentId }], {
2074
+ policyOnly,
2075
+ dryRun: true,
2076
+ });
2077
+ expect(preview.updated).toEqual([agentId]);
2078
+ expect(
2079
+ (await client.getDeployment(NAMESPACE, name)).spec.template.metadata
2080
+ .labels["vgs.io/team"],
2081
+ ).toBe("NOT_SET");
2082
+ expect(
2083
+ (await next.reconcile([{ id: agentId }], { policyOnly })).updated,
2084
+ ).toEqual([agentId]);
2085
+ const after = await client.getDeployment(NAMESPACE, name);
2086
+ expect(after.spec.selector).toEqual(selector);
2087
+ expect(after.spec.template.metadata.labels).toMatchObject({
2088
+ ...podLabels,
2089
+ "other.io/label": "preserved",
2090
+ });
2091
+ expect(
2092
+ (await next.reconcile([{ id: agentId }], { policyOnly })).updated,
2093
+ ).toEqual([]);
2094
+ });
2095
+ }
2096
+ it("gives newly provisioned agents the configured labels", async () => {
2097
+ const client = emptyClient();
2098
+ await new KubernetesAgentProvisioner(
2099
+ client,
2100
+ stubTokens() as AgentTokenService,
2101
+ { ...BASE_CONFIG, podLabels },
2102
+ ).provision(agentId);
2103
+ expect(
2104
+ (await client.getDeployment(NAMESPACE, sanitizeAgentName(agentId))).spec
2105
+ .template.metadata.labels,
2106
+ ).toMatchObject(podLabels);
2107
+ });
2108
+ });
package/src/api.ts CHANGED
@@ -66,7 +66,7 @@ export interface AgentConfigResponse {
66
66
  */
67
67
  accountStatus?: string | null;
68
68
  /**
69
- * ARD-6.1: the agent's Specialty name (e.g. "coding"), read by the agent
69
+ * ARD-6.1: the agent's Specialty name (e.g. "specialist"), read by the agent
70
70
  * runtime to pick its workspace template directory.
71
71
  */
72
72
  specialty: string;
@@ -11,7 +11,9 @@
11
11
  * agents lose the mount after the policy is disabled. The same pass applies
12
12
  * the Anthropic workload identity federation wiring (ServiceAccount, projected
13
13
  * token volume, mount, federation env), so turning federation on or off and
14
- * restarting admin corrects existing agents.
14
+ * restarting admin corrects existing agents. Configured pod labels are also
15
+ * reconciled, preserving selectors and labels owned by other actors; changing
16
+ * a configured label rolls the affected agent through its Recreate strategy.
15
17
  *
16
18
  * The startup pass never moves agents onto their own per-agent identity: that
17
19
  * creates Anthropic and Kubernetes resources, which should not happen as a side