@stigmer/server 3.18.0-dev.20260918103812 → 3.18.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.
Files changed (75) hide show
  1. package/dist/boot/compose.d.ts +12 -0
  2. package/dist/boot/compose.d.ts.map +1 -1
  3. package/dist/boot/compose.js +4 -2
  4. package/dist/boot/compose.js.map +1 -1
  5. package/dist/domain/agentexecution/lifecycle.js +1 -1
  6. package/dist/domain/agentexecution/lifecycle.js.map +1 -1
  7. package/dist/domain/iampolicy/constants.d.ts +3 -3
  8. package/dist/domain/iampolicy/constants.js +3 -3
  9. package/dist/domain/oauthapp/refresolution/refresolution.js +1 -1
  10. package/dist/domain/oauthapp/refresolution.js +1 -1
  11. package/dist/domain/platform/controller.d.ts +8 -1
  12. package/dist/domain/platform/controller.d.ts.map +1 -1
  13. package/dist/domain/platform/controller.js +2 -3
  14. package/dist/domain/platform/controller.js.map +1 -1
  15. package/dist/domain/plugin/constants.d.ts +7 -1
  16. package/dist/domain/plugin/constants.d.ts.map +1 -1
  17. package/dist/domain/plugin/constants.js +7 -1
  18. package/dist/domain/plugin/constants.js.map +1 -1
  19. package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
  20. package/dist/domain/plugin/materialize/plan.js +16 -1
  21. package/dist/domain/plugin/materialize/plan.js.map +1 -1
  22. package/dist/domain/plugin/members.d.ts +46 -0
  23. package/dist/domain/plugin/members.d.ts.map +1 -1
  24. package/dist/domain/plugin/members.js +31 -8
  25. package/dist/domain/plugin/members.js.map +1 -1
  26. package/dist/domain/plugin/push.d.ts +5 -3
  27. package/dist/domain/plugin/push.d.ts.map +1 -1
  28. package/dist/domain/plugin/push.js +58 -14
  29. package/dist/domain/plugin/push.js.map +1 -1
  30. package/dist/domain/workflowexecution/lifecycle.js +1 -1
  31. package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
  32. package/dist/pipeline/apiresource-labels.d.ts +20 -1
  33. package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
  34. package/dist/pipeline/apiresource-labels.js +17 -0
  35. package/dist/pipeline/apiresource-labels.js.map +1 -1
  36. package/dist/sandbox/kubernetes.d.ts.map +1 -1
  37. package/dist/sandbox/kubernetes.js +1 -8
  38. package/dist/sandbox/kubernetes.js.map +1 -1
  39. package/dist/sandbox/provisioner.d.ts +5 -20
  40. package/dist/sandbox/provisioner.d.ts.map +1 -1
  41. package/dist/sandbox/provisioner.js.map +1 -1
  42. package/dist/sandbox/steps.d.ts +8 -22
  43. package/dist/sandbox/steps.d.ts.map +1 -1
  44. package/dist/sandbox/steps.js +12 -17
  45. package/dist/sandbox/steps.js.map +1 -1
  46. package/dist/store/interface.d.ts +6 -2
  47. package/dist/store/interface.d.ts.map +1 -1
  48. package/dist/store/interface.js.map +1 -1
  49. package/package.json +5 -5
  50. package/src/boot/compose.ts +16 -2
  51. package/src/domain/agentexecution/lifecycle.ts +1 -1
  52. package/src/domain/iampolicy/constants.ts +3 -3
  53. package/src/domain/oauthapp/__tests__/oauthapp.test.ts +2 -2
  54. package/src/domain/oauthapp/__tests__/refresolution.test.ts +2 -2
  55. package/src/domain/oauthapp/refresolution/refresolution.ts +1 -1
  56. package/src/domain/oauthapp/refresolution.ts +1 -1
  57. package/src/domain/platform/__tests__/platform.test.ts +47 -0
  58. package/src/domain/platform/controller.ts +10 -4
  59. package/src/domain/plugin/__tests__/materialize.test.ts +42 -47
  60. package/src/domain/plugin/__tests__/members.test.ts +149 -0
  61. package/src/domain/plugin/__tests__/plugin.test.ts +219 -8
  62. package/src/domain/plugin/constants.ts +7 -1
  63. package/src/domain/plugin/materialize/plan.ts +20 -1
  64. package/src/domain/plugin/members.ts +76 -7
  65. package/src/domain/plugin/push.ts +80 -20
  66. package/src/domain/workflowexecution/lifecycle.ts +1 -1
  67. package/src/pipeline/apiresource-labels.ts +26 -1
  68. package/src/sandbox/__tests__/docker.integration.test.ts +0 -2
  69. package/src/sandbox/__tests__/kubernetes.test.ts +2 -10
  70. package/src/sandbox/__tests__/local-process.integration.test.ts +0 -3
  71. package/src/sandbox/__tests__/steps.test.ts +19 -72
  72. package/src/sandbox/kubernetes.ts +1 -8
  73. package/src/sandbox/provisioner.ts +5 -20
  74. package/src/sandbox/steps.ts +12 -29
  75. package/src/store/interface.ts +6 -2
@@ -15,6 +15,9 @@
15
15
  * execution id;
16
16
  * - getRunnerBootstrapConfig echoes the configured Temporal coordinates
17
17
  * with the token fields empty (minting a proxy token is cloud-only);
18
+ * - getServerInfo reports the build stamp by default and the release a
19
+ * library consumer states through ComposeOptions.version otherwise
20
+ * (stigmer#1168: unbundled, the stamp is always "dev");
18
21
  * - getLicenseStatus answers `absent` with no claims and no key when no
19
22
  * provider is composed, and the composed provider's report verbatim
20
23
  * when one is — both stamped with the ONE instant the controller
@@ -229,6 +232,48 @@ describe("platform domain (composed server)", () => {
229
232
  });
230
233
  });
231
234
 
235
+ describe("platform domain (composed with a stated version)", () => {
236
+ let server: ComposedServer;
237
+ let client: PlatformClient;
238
+ let dir: string;
239
+
240
+ beforeAll(async () => {
241
+ dir = mkdtempSync(path.join(tmpdir(), "platform-domain-version-test-"));
242
+ vi.stubEnv("STIGMER_ENCRYPTION_KEY", ENC_KEY.toString("base64"));
243
+ server = await composeServer({
244
+ config: loadConfig({
245
+ STIGMER_MODEL_REGISTRY_REFRESH: "off",
246
+ DB_PATH: path.join(dir, "stigmer.db"),
247
+ ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
248
+ }),
249
+ logger: silentLogger,
250
+ version: "3.99.0",
251
+ portOverride: 0,
252
+ host: "127.0.0.1",
253
+ });
254
+ const port = await server.start();
255
+ client = createClient(
256
+ PlatformQueryController,
257
+ createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` }),
258
+ );
259
+ });
260
+
261
+ afterAll(async () => {
262
+ await server.shutdown();
263
+ rmSync(dir, { recursive: true, force: true });
264
+ vi.unstubAllEnvs();
265
+ });
266
+
267
+ it("getServerInfo reports the composition's stated release, not the build stamp", async () => {
268
+ // A library consumer (the cloud composition) runs no bundle, so the
269
+ // define is never stamped for it; ComposeOptions.version is how it
270
+ // states the release it installed. Edition is untouched by it.
271
+ const info = await client.getServerInfo({});
272
+ expect(info.version).toBe("3.99.0");
273
+ expect(info.edition).toBe(ServerEdition.oss);
274
+ });
275
+ });
276
+
232
277
  describe("platform domain (keyless runner-token service)", () => {
233
278
  it("a keyless service answers not-minted — fail-soft, never an error", async () => {
234
279
  const transport = createRouterTransport((router) => {
@@ -239,6 +284,7 @@ describe("platform domain (keyless runner-token service)", () => {
239
284
  RunnerAuthService.create(undefined),
240
285
  ),
241
286
  edition: ServerEdition.oss,
287
+ version: "dev",
242
288
  licenseStatus: ABSENT_LICENSE_STATUS,
243
289
  now: () => new Date(),
244
290
  logger: silentLogger,
@@ -273,6 +319,7 @@ describe("platform domain (license status)", () => {
273
319
  RunnerAuthService.create(undefined),
274
320
  ),
275
321
  edition: ServerEdition.enterprise,
322
+ version: "dev",
276
323
  licenseStatus: provider,
277
324
  now: () => instant,
278
325
  logger: silentLogger,
@@ -7,7 +7,7 @@
7
7
  * getRunnerBootstrapConfig and getLicenseStatus shapes; getRunnerScopedToken
8
8
  * is deliberately excluded there — its arms are exercised mid-execution)
9
9
  * and __tests__/platform.test.ts (the fail-soft mint matrix, the license
10
- * status arms, the #15 composed-server pattern).
10
+ * status arms, the stated-version arm, the #15 composed-server pattern).
11
11
  *
12
12
  * getLicenseStatus is the one arm every edition answers from a driver
13
13
  * point: a server's license is a fact about the server, like its edition,
@@ -75,7 +75,6 @@ import type {
75
75
  RunnerScopedTokenRequest,
76
76
  } from "../../runnerauth/runner-credential-provider.js";
77
77
  import { TOKEN_TYPE_EXECUTION_SCOPED } from "../../runnerauth/runnerauth.js";
78
- import { SERVER_VERSION } from "./version.js";
79
78
 
80
79
  export interface PlatformControllerDeps {
81
80
  /**
@@ -100,6 +99,13 @@ export interface PlatformControllerDeps {
100
99
  * cloud composition answers `cloud` without forking this controller.
101
100
  */
102
101
  readonly edition: ServerEdition;
102
+ /**
103
+ * The release reported beside the edition. The composition root resolves
104
+ * it (ComposeOptions.version, else SERVER_VERSION), so this controller
105
+ * never reads the build stamp itself: a library consumer with no bundle
106
+ * states its installed release and answers exactly as a bundle would.
107
+ */
108
+ readonly version: string;
103
109
  /**
104
110
  * What license this server holds (extensions/license-status.ts). The
105
111
  * composition root installs the built-in `absent` provider when no unit
@@ -130,11 +136,11 @@ export function registerPlatformServices(
130
136
  });
131
137
  }
132
138
 
133
- /** Go GetServerInfo: the server edition and build version. */
139
+ /** Go GetServerInfo: the server edition and release version. */
134
140
  function getServerInfo(deps: PlatformControllerDeps): GetServerInfoOutput {
135
141
  return create(GetServerInfoOutputSchema, {
136
142
  edition: deps.edition,
137
- version: SERVER_VERSION,
143
+ version: deps.version,
138
144
  });
139
145
  }
140
146
 
@@ -8,8 +8,10 @@
8
8
  * SKILL.md with the plugin's labels on the request, a version the tag
9
9
  * pattern rejects becoming a warning, an overlay that redefines the
10
10
  * transport refused, the composed agent declaring its tools' variables
11
- * (OAuth targets excluded, an authored agent left as written), and the
12
- * convergence and drop rules over members.
11
+ * (OAuth targets excluded, an authored agent left as written), and each
12
+ * planned member's system flag read from its overlay and never from a
13
+ * skill. The member rules themselves (the slug decision, convergence, the
14
+ * dropped set) are members.test.ts's.
13
15
  */
14
16
  import { describe, expect, it } from "vitest";
15
17
 
@@ -37,6 +39,7 @@ import { openArchive } from "../../../archive/open.js";
37
39
  import {
38
40
  PLUGIN_LABEL,
39
41
  PLUGIN_VERSION_LABEL,
42
+ SYSTEM_LABEL,
40
43
  } from "../../../pipeline/apiresource-labels.js";
41
44
  import {
42
45
  DEFAULT_AGENT_INSTRUCTIONS_VERSION,
@@ -45,8 +48,6 @@ import {
45
48
  import type { PluginIdentity } from "../materialize/identity.js";
46
49
  import { McpServerOverlayError } from "../materialize/mcp-servers.js";
47
50
  import { planMaterialization } from "../materialize/plan.js";
48
- import { droppedMembers, membersConverge } from "../members.js";
49
- import type { Member } from "../members.js";
50
51
 
51
52
  const IDENTITY: PluginIdentity = {
52
53
  org: "acme",
@@ -231,7 +232,10 @@ describe("planMaterialization", () => {
231
232
 
232
233
  it("declares the servers' variables on the composed agent so a session asks for them", () => {
233
234
  expect(plan.agent!.resource.spec?.env).toEqual({
234
- GITHUB_TOKEN: expect.objectContaining({ isSecret: true, optional: false }),
235
+ GITHUB_TOKEN: expect.objectContaining({
236
+ isSecret: true,
237
+ optional: false,
238
+ }),
235
239
  });
236
240
  });
237
241
 
@@ -259,7 +263,9 @@ describe("planMaterialization", () => {
259
263
  );
260
264
  // The server still declares it (the connect flow reads it there); the
261
265
  // agent does not, because the value comes from a managed environment.
262
- expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty("GITHUB_TOKEN");
266
+ expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty(
267
+ "GITHUB_TOKEN",
268
+ );
263
269
  expect(withOAuth.agent!.resource.spec?.env).toEqual({});
264
270
  });
265
271
 
@@ -313,50 +319,39 @@ describe("planMaterialization", () => {
313
319
  });
314
320
  });
315
321
 
316
- describe("members: convergence and drops", () => {
317
- const digest = "d".repeat(64);
318
- const stored: Member[] = [
319
- {
320
- kind: ApiResourceKind.skill,
321
- id: "skl_1",
322
- slug: "thermo-review",
323
- name: "thermo-review",
324
- version: digest,
325
- },
326
- {
327
- kind: ApiResourceKind.agent,
328
- id: "agt_1",
329
- slug: "thermos",
330
- name: "thermos",
331
- version: digest,
332
- },
333
- ];
334
- const planned = [
335
- {
336
- kind: ApiResourceKind.skill,
337
- slug: "thermo-review",
338
- name: "thermo-review",
339
- },
340
- { kind: ApiResourceKind.agent, slug: "thermos", name: "thermos" },
341
- ];
322
+ describe("planned members: the system flag", () => {
323
+ const pkg = read(thermos);
342
324
 
343
- it("converges only when every planned member is present and stamped with the digest", () => {
344
- expect(membersConverge(stored, planned, digest)).toBe(true);
345
- expect(membersConverge(stored, planned, "e".repeat(64))).toBe(false);
346
- expect(membersConverge(stored.slice(1), planned, digest)).toBe(false);
347
- expect(
348
- membersConverge(
349
- [...stored, { ...stored[0]!, slug: "extra", id: "skl_2" }],
350
- planned,
351
- digest,
352
- ),
353
- ).toBe(false);
325
+ it("reads system content from the overlay's labels, never from a skill's request", () => {
326
+ const authored = create(AgentSchema, {
327
+ metadata: { name: "thermos", labels: { [SYSTEM_LABEL]: "true" } },
328
+ spec: create(AgentSpecSchema, { instructions: "You read thermostats." }),
329
+ });
330
+ const plan = planMaterialization(
331
+ pkg,
332
+ inMemoryPluginFiles(thermos),
333
+ {
334
+ agent: { path: "ai.stigmer/agent.yaml", resource: authored },
335
+ workflows: [],
336
+ mcpServers: [],
337
+ },
338
+ IDENTITY,
339
+ "",
340
+ );
341
+ const byKind = new Map(plan.members.map((m) => [m.kind, m.system]));
342
+ expect(byKind.get(ApiResourceKind.agent)).toBe(true);
343
+ expect(byKind.get(ApiResourceKind.skill)).toBe(false);
344
+ expect(byKind.get(ApiResourceKind.mcp_server)).toBe(false);
354
345
  });
355
346
 
356
- it("drops the members the plan no longer names", () => {
357
- expect(droppedMembers(stored, planned.slice(1)).map((m) => m.slug)).toEqual(
358
- ["thermo-review"],
347
+ it("plans no system content when no overlay declares it", () => {
348
+ const plan = planMaterialization(
349
+ pkg,
350
+ inMemoryPluginFiles(thermos),
351
+ { workflows: [], mcpServers: [] },
352
+ IDENTITY,
353
+ "",
359
354
  );
360
- expect(droppedMembers(stored, planned)).toEqual([]);
355
+ expect(plan.members.every((m) => m.system === false)).toBe(true);
361
356
  });
362
357
  });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Pins the member rules on plain values, no store: the slug decision
3
+ * (`judgeSlug`) in each of its five arms — free, ours, adopt when system
4
+ * content meets system content, refused for an unmanaged row whenever
5
+ * either side lacks the label, refused for another plugin's member
6
+ * whatever labels either side carries — and the convergence and drop rules
7
+ * over stored and planned members. The same decision through a composed
8
+ * server, with a real authorizer, is plugin.test.ts's.
9
+ */
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
13
+
14
+ import { droppedMembers, judgeSlug, membersConverge } from "../members.js";
15
+ import type { Member, PlannedMember, SlugHolder } from "../members.js";
16
+
17
+ const DIGEST = "d".repeat(64);
18
+ const OURS = "plg_ours";
19
+ const OTHER = "plg_other";
20
+
21
+ function stored(overrides: Partial<Member> = {}): Member {
22
+ return {
23
+ kind: ApiResourceKind.agent,
24
+ id: "agt_1",
25
+ slug: "assistant",
26
+ name: "assistant",
27
+ version: DIGEST,
28
+ system: false,
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ function planned(overrides: Partial<PlannedMember> = {}): PlannedMember {
34
+ return {
35
+ kind: ApiResourceKind.agent,
36
+ slug: "assistant",
37
+ name: "assistant",
38
+ system: false,
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ function heldBy(
44
+ byPlugin: string | undefined,
45
+ holder: Member = stored(),
46
+ ): SlugHolder {
47
+ return { held: true, byPlugin, holder };
48
+ }
49
+
50
+ describe("judgeSlug", () => {
51
+ it("is free when nothing holds the slug", () => {
52
+ expect(judgeSlug({ held: false }, planned(), OURS)).toEqual({
53
+ kind: "free",
54
+ });
55
+ });
56
+
57
+ it("is ours when this plugin already holds it, whatever the labels", () => {
58
+ const holder = stored({ system: true });
59
+ expect(judgeSlug(heldBy(OURS, holder), planned(), OURS)).toEqual({
60
+ kind: "ours",
61
+ holder,
62
+ });
63
+ });
64
+
65
+ it("adopts an unmanaged row only when both sides are system content", () => {
66
+ const holder = stored({ system: true });
67
+ expect(
68
+ judgeSlug(heldBy(undefined, holder), planned({ system: true }), OURS),
69
+ ).toEqual({ kind: "adopt", holder });
70
+ });
71
+
72
+ it("refuses an unmanaged row when either side lacks the system label", () => {
73
+ const userRow = stored({ system: false });
74
+ expect(
75
+ judgeSlug(heldBy(undefined, userRow), planned({ system: true }), OURS),
76
+ ).toEqual({ kind: "held-unmanaged", holder: userRow });
77
+
78
+ const systemRow = stored({ system: true });
79
+ expect(
80
+ judgeSlug(heldBy(undefined, systemRow), planned({ system: false }), OURS),
81
+ ).toEqual({ kind: "held-unmanaged", holder: systemRow });
82
+
83
+ expect(
84
+ judgeSlug(heldBy(undefined, userRow), planned({ system: false }), OURS),
85
+ ).toEqual({ kind: "held-unmanaged", holder: userRow });
86
+ });
87
+
88
+ it("never transfers another plugin's member, even system content to system content", () => {
89
+ const holder = stored({ system: true });
90
+ expect(
91
+ judgeSlug(heldBy(OTHER, holder), planned({ system: true }), OURS),
92
+ ).toEqual({ kind: "held-by-plugin", holder, pluginId: OTHER });
93
+ });
94
+ });
95
+
96
+ describe("members: convergence and drops", () => {
97
+ const stored: Member[] = [
98
+ {
99
+ kind: ApiResourceKind.skill,
100
+ id: "skl_1",
101
+ slug: "thermo-review",
102
+ name: "thermo-review",
103
+ version: DIGEST,
104
+ system: false,
105
+ },
106
+ {
107
+ kind: ApiResourceKind.agent,
108
+ id: "agt_1",
109
+ slug: "thermos",
110
+ name: "thermos",
111
+ version: DIGEST,
112
+ system: false,
113
+ },
114
+ ];
115
+ const planned: PlannedMember[] = [
116
+ {
117
+ kind: ApiResourceKind.skill,
118
+ slug: "thermo-review",
119
+ name: "thermo-review",
120
+ system: false,
121
+ },
122
+ {
123
+ kind: ApiResourceKind.agent,
124
+ slug: "thermos",
125
+ name: "thermos",
126
+ system: false,
127
+ },
128
+ ];
129
+
130
+ it("converges only when every planned member is present and stamped with the digest", () => {
131
+ expect(membersConverge(stored, planned, DIGEST)).toBe(true);
132
+ expect(membersConverge(stored, planned, "e".repeat(64))).toBe(false);
133
+ expect(membersConverge(stored.slice(1), planned, DIGEST)).toBe(false);
134
+ expect(
135
+ membersConverge(
136
+ [...stored, { ...stored[0]!, slug: "extra", id: "skl_2" }],
137
+ planned,
138
+ DIGEST,
139
+ ),
140
+ ).toBe(false);
141
+ });
142
+
143
+ it("drops the members the plan no longer names", () => {
144
+ expect(droppedMembers(stored, planned.slice(1)).map((m) => m.slug)).toEqual(
145
+ ["thermo-review"],
146
+ );
147
+ expect(droppedMembers(stored, planned)).toEqual([]);
148
+ });
149
+ });
@@ -9,10 +9,14 @@
9
9
  * delete of a member is refused naming the plugin while the plugin's own
10
10
  * upgrade drops what the archive dropped; uninstall removes every member
11
11
  * and is refused while a user's own agent still references one; a slug an
12
- * unmanaged resource holds refuses the install; a bad overlay refuses
13
- * before any write. A second composition, under an authorizer that denies
14
- * `can_write_reserved_labels`, pins the reserved-label refusal the
15
- * open-source posture allows by design.
12
+ * unmanaged resource holds refuses the install, and a user's row is never
13
+ * adopted whichever side declares system content; a bad overlay refuses
14
+ * before any write. A second composition, under an authorizer that decides
15
+ * `can_write_reserved_labels` per test, pins the reserved-label refusal the
16
+ * open-source posture allows by design, and the one adoption the platform
17
+ * makes: an operator's plugin taking over the system-content row it
18
+ * replaces in place, the row's id and default instance kept, a member's
19
+ * identical push refused before the slug is judged.
16
20
  */
17
21
  import { mkdtempSync, rmSync } from "node:fs";
18
22
  import net from "node:net";
@@ -38,6 +42,7 @@ import {
38
42
  AgentSpecSchema,
39
43
  McpServerUsageSchema,
40
44
  } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
45
+ import { AgentInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/query_pb";
41
46
  import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
42
47
  import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
43
48
  import { PluginCommandController } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/command_pb";
@@ -59,6 +64,7 @@ import { createLogger } from "../../../boot/logger.js";
59
64
  import {
60
65
  PLUGIN_LABEL,
61
66
  PLUGIN_VERSION_LABEL,
67
+ SYSTEM_LABEL,
62
68
  } from "../../../pipeline/apiresource-labels.js";
63
69
 
64
70
  const silentLogger = createLogger({
@@ -507,6 +513,66 @@ describe("Plugin members are the plugin's to redefine", () => {
507
513
  );
508
514
  });
509
515
 
516
+ it("never adopts a user's agent: a same-slug row without the system label is refused even when the plugin declares system content, and a system row is refused by a plugin that does not", async () => {
517
+ // The open-source authorizer grants reserved-label writes to everyone,
518
+ // so this is the posture where the label alone would be a weak guard;
519
+ // the rule needs BOTH sides to carry it.
520
+ const name = uniqueName("users-own");
521
+ await agents.create(
522
+ createMessage(AgentSchema, {
523
+ apiVersion: "agentic.stigmer.ai/v1",
524
+ kind: "Agent",
525
+ metadata: createMessage(ApiResourceMetadataSchema, { org: ORG, name }),
526
+ spec: createMessage(AgentSpecSchema, {
527
+ instructions: "Mine, written by hand, under my own name.",
528
+ }),
529
+ }),
530
+ );
531
+ await expectCode(
532
+ plugins.push({ org: ORG, artifact: archiveOf(systemOverlay(name)) }),
533
+ Code.AlreadyExists,
534
+ `'${name}' exists in org '${ORG}' and is not managed by a plugin`,
535
+ );
536
+
537
+ const seeded = uniqueName("seeded-row");
538
+ await agents.create(
539
+ createMessage(AgentSchema, {
540
+ apiVersion: "agentic.stigmer.ai/v1",
541
+ kind: "Agent",
542
+ metadata: createMessage(ApiResourceMetadataSchema, {
543
+ org: ORG,
544
+ name: seeded,
545
+ labels: { [SYSTEM_LABEL]: "true" },
546
+ }),
547
+ spec: createMessage(AgentSpecSchema, {
548
+ instructions: "System content a plain plugin may not take over.",
549
+ }),
550
+ }),
551
+ );
552
+ await expectCode(
553
+ plugins.push({
554
+ org: ORG,
555
+ artifact: archiveOf(
556
+ withFile(
557
+ thermosLike(seeded),
558
+ "ai.stigmer/agent.yaml",
559
+ [
560
+ "apiVersion: agentic.stigmer.ai/v1",
561
+ "kind: Agent",
562
+ "metadata:",
563
+ ` name: ${seeded}`,
564
+ "spec:",
565
+ " instructions: A plain overlay that declares no system content.",
566
+ "",
567
+ ].join("\n"),
568
+ ),
569
+ ),
570
+ }),
571
+ Code.AlreadyExists,
572
+ `'${seeded}' exists in org '${ORG}' and is not managed by a plugin`,
573
+ );
574
+ });
575
+
510
576
  it("refuses to install a slug another plugin holds, naming that plugin", async () => {
511
577
  const first = uniqueName("holder");
512
578
  const second = uniqueName("claimant");
@@ -705,7 +771,14 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
705
771
  let enforcing: ComposedServer;
706
772
  let enforcingPlugins: Client<typeof PluginCommandController>;
707
773
  let enforcingQuery: Client<typeof PluginQueryController>;
774
+ let enforcingAgents: Client<typeof AgentCommandController>;
775
+ let enforcingAgentQuery: Client<typeof AgentQueryController>;
776
+ let enforcingInstanceQuery: Client<typeof AgentInstanceQueryController>;
708
777
  const observed: AuthzCheck[] = [];
778
+ // Who the caller is to this authorizer: a member (reserved labels denied,
779
+ // the hosted default) or the platform operator (granted). The tests flip
780
+ // it, so one composition shows both sides of every reserved-label rule.
781
+ let reservedLabels: "deny" | "allow" = "deny";
709
782
 
710
783
  beforeAll(async () => {
711
784
  enforcingDir = mkdtempSync(path.join(tmpdir(), "plugin-domain-enforcing-"));
@@ -714,10 +787,11 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
714
787
  authorize(_caller, check) {
715
788
  if (check.permission === IamPermission.can_write_reserved_labels) {
716
789
  observed.push(check);
717
- return Promise.resolve({
718
- kind: "deny",
719
- reason: "reserved labels are the platform's",
720
- });
790
+ return Promise.resolve(
791
+ reservedLabels === "allow"
792
+ ? { kind: "allow" }
793
+ : { kind: "deny", reason: "reserved labels are the platform's" },
794
+ );
721
795
  }
722
796
  return Promise.resolve({ kind: "allow" });
723
797
  },
@@ -741,6 +815,12 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
741
815
  });
742
816
  enforcingPlugins = createClient(PluginCommandController, transport);
743
817
  enforcingQuery = createClient(PluginQueryController, transport);
818
+ enforcingAgents = createClient(AgentCommandController, transport);
819
+ enforcingAgentQuery = createClient(AgentQueryController, transport);
820
+ enforcingInstanceQuery = createClient(
821
+ AgentInstanceQueryController,
822
+ transport,
823
+ );
744
824
  });
745
825
 
746
826
  afterAll(async () => {
@@ -799,4 +879,135 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
799
879
  });
800
880
  expect(members).toHaveLength(3);
801
881
  });
882
+
883
+ it("adopts a system-content row the operator's plugin replaces: same id, same default instance, the plugin's definition, one warning", async () => {
884
+ reservedLabels = "allow";
885
+ const name = uniqueName("plg-adopt");
886
+ // The row the platform seeded before plugins existed: system content,
887
+ // no plugin label, a default instance of its own.
888
+ const seeded = await enforcingAgents.create(
889
+ createMessage(AgentSchema, {
890
+ apiVersion: "agentic.stigmer.ai/v1",
891
+ kind: "Agent",
892
+ metadata: createMessage(ApiResourceMetadataSchema, {
893
+ org: ORG,
894
+ name,
895
+ labels: { [SYSTEM_LABEL]: "true" },
896
+ }),
897
+ spec: createMessage(AgentSpecSchema, {
898
+ instructions: "The instructions the seedpack wrote, long ago.",
899
+ }),
900
+ }),
901
+ );
902
+ const seededId = seeded.metadata!.id;
903
+ const seededInstanceId = seeded.status?.defaultInstanceId ?? "";
904
+ expect(seededInstanceId).not.toBe("");
905
+
906
+ const installed = await enforcingPlugins.push({
907
+ org: ORG,
908
+ artifact: archiveOf(systemOverlay(name)),
909
+ });
910
+ expect(installed.status?.state).toBe(PluginState.READY);
911
+ const adopted = installed.status!.warnings.filter(
912
+ (warning) => warning.kind === "member-adopted",
913
+ );
914
+ expect(adopted).toHaveLength(1);
915
+ expect(adopted[0]!.path).toBe("ai.stigmer/agent.yaml");
916
+ expect(adopted[0]!.message).toContain(`agent '${name}' (${seededId})`);
917
+ expect(adopted[0]!.message).toContain("conversations continue");
918
+
919
+ const after = await enforcingAgentQuery.get({ value: seededId });
920
+ expect(after.metadata?.labels[PLUGIN_LABEL]).toBe(installed.metadata!.id);
921
+ expect(after.metadata?.labels[PLUGIN_VERSION_LABEL]).toBe(
922
+ installed.status!.digest,
923
+ );
924
+ expect(after.metadata?.labels[SYSTEM_LABEL]).toBe("true");
925
+ expect(after.spec?.instructions).toBe(
926
+ "You are the platform's own agent, re-homed under a plugin.",
927
+ );
928
+ expect(after.status?.defaultInstanceId).toBe(seededInstanceId);
929
+ const instance = await enforcingInstanceQuery.get({
930
+ value: seededInstanceId,
931
+ });
932
+ expect(instance.spec?.agentId).toBe(seededId);
933
+
934
+ const { members } = await enforcingQuery.listMembers({
935
+ value: installed.metadata!.id,
936
+ });
937
+ expect(
938
+ members.find((member) => member.kind === ApiResourceKind.agent)?.id,
939
+ ).toBe(seededId);
940
+
941
+ // The second push of the same archive converges without a write.
942
+ const again = await enforcingPlugins.push({
943
+ org: ORG,
944
+ artifact: archiveOf(systemOverlay(name)),
945
+ });
946
+ expect(again.status?.digest).toBe(installed.status?.digest);
947
+ expect(
948
+ (await enforcingAgentQuery.get({ value: seededId })).status
949
+ ?.defaultInstanceId,
950
+ ).toBe(seededInstanceId);
951
+ reservedLabels = "deny";
952
+ });
953
+
954
+ it("refuses the same archive from a member before the slug is judged, and leaves the seeded row untouched", async () => {
955
+ reservedLabels = "allow";
956
+ const name = uniqueName("plg-member");
957
+ const seeded = await enforcingAgents.create(
958
+ createMessage(AgentSchema, {
959
+ apiVersion: "agentic.stigmer.ai/v1",
960
+ kind: "Agent",
961
+ metadata: createMessage(ApiResourceMetadataSchema, {
962
+ org: ORG,
963
+ name,
964
+ labels: { [SYSTEM_LABEL]: "true" },
965
+ }),
966
+ spec: createMessage(AgentSpecSchema, {
967
+ instructions: "The instructions the seedpack wrote, long ago.",
968
+ }),
969
+ }),
970
+ );
971
+ reservedLabels = "deny";
972
+ const error = await expectCode(
973
+ enforcingPlugins.push({
974
+ org: ORG,
975
+ artifact: archiveOf(systemOverlay(name)),
976
+ }),
977
+ Code.InvalidArgument,
978
+ );
979
+ expect(error.rawMessage).toContain(
980
+ `cannot be set by a plugin (ai.stigmer/agent.yaml: ${SYSTEM_LABEL})`,
981
+ );
982
+ const untouched = await enforcingAgentQuery.get({
983
+ value: seeded.metadata!.id,
984
+ });
985
+ expect(untouched.metadata?.labels[PLUGIN_LABEL]).toBeUndefined();
986
+ expect(untouched.spec?.instructions).toBe(
987
+ "The instructions the seedpack wrote, long ago.",
988
+ );
989
+ });
802
990
  });
991
+
992
+ /**
993
+ * A plugin whose agent overlay declares system content under the given
994
+ * name: the shape of the official `assistant` plugin, whose agent replaces
995
+ * the row the retired seedpack wrote under the same slug.
996
+ */
997
+ function systemOverlay(name: string): PluginFixture {
998
+ return withFile(
999
+ thermosLike(name),
1000
+ "ai.stigmer/agent.yaml",
1001
+ [
1002
+ "apiVersion: agentic.stigmer.ai/v1",
1003
+ "kind: Agent",
1004
+ "metadata:",
1005
+ ` name: ${name}`,
1006
+ " labels:",
1007
+ ` ${SYSTEM_LABEL}: "true"`,
1008
+ "spec:",
1009
+ " instructions: You are the platform's own agent, re-homed under a plugin.",
1010
+ "",
1011
+ ].join("\n"),
1012
+ );
1013
+ }