@stigmer/server 3.17.0 → 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 (53) 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/iampolicy/constants.d.ts +3 -3
  6. package/dist/domain/iampolicy/constants.js +3 -3
  7. package/dist/domain/oauthapp/refresolution/refresolution.js +1 -1
  8. package/dist/domain/oauthapp/refresolution.js +1 -1
  9. package/dist/domain/platform/controller.d.ts +8 -1
  10. package/dist/domain/platform/controller.d.ts.map +1 -1
  11. package/dist/domain/platform/controller.js +2 -3
  12. package/dist/domain/platform/controller.js.map +1 -1
  13. package/dist/domain/plugin/constants.d.ts +7 -1
  14. package/dist/domain/plugin/constants.d.ts.map +1 -1
  15. package/dist/domain/plugin/constants.js +7 -1
  16. package/dist/domain/plugin/constants.js.map +1 -1
  17. package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
  18. package/dist/domain/plugin/materialize/plan.js +16 -1
  19. package/dist/domain/plugin/materialize/plan.js.map +1 -1
  20. package/dist/domain/plugin/members.d.ts +46 -0
  21. package/dist/domain/plugin/members.d.ts.map +1 -1
  22. package/dist/domain/plugin/members.js +31 -8
  23. package/dist/domain/plugin/members.js.map +1 -1
  24. package/dist/domain/plugin/push.d.ts +5 -3
  25. package/dist/domain/plugin/push.d.ts.map +1 -1
  26. package/dist/domain/plugin/push.js +58 -14
  27. package/dist/domain/plugin/push.js.map +1 -1
  28. package/dist/pipeline/apiresource-labels.d.ts +20 -1
  29. package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
  30. package/dist/pipeline/apiresource-labels.js +17 -0
  31. package/dist/pipeline/apiresource-labels.js.map +1 -1
  32. package/dist/store/interface.d.ts +6 -2
  33. package/dist/store/interface.d.ts.map +1 -1
  34. package/dist/store/interface.js.map +1 -1
  35. package/package.json +5 -5
  36. package/src/boot/compose.ts +16 -2
  37. package/src/domain/iampolicy/constants.ts +3 -3
  38. package/src/domain/oauthapp/__tests__/oauthapp.test.ts +2 -2
  39. package/src/domain/oauthapp/__tests__/refresolution.test.ts +2 -2
  40. package/src/domain/oauthapp/refresolution/refresolution.ts +1 -1
  41. package/src/domain/oauthapp/refresolution.ts +1 -1
  42. package/src/domain/platform/__tests__/platform.test.ts +47 -0
  43. package/src/domain/platform/controller.ts +10 -4
  44. package/src/domain/plugin/__tests__/materialize.test.ts +42 -47
  45. package/src/domain/plugin/__tests__/members.test.ts +149 -0
  46. package/src/domain/plugin/__tests__/plugin.test.ts +219 -8
  47. package/src/domain/plugin/constants.ts +7 -1
  48. package/src/domain/plugin/materialize/plan.ts +20 -1
  49. package/src/domain/plugin/members.ts +76 -7
  50. package/src/domain/plugin/push.ts +80 -20
  51. package/src/pipeline/apiresource-labels.ts +26 -1
  52. package/src/sandbox/__tests__/local-process.integration.test.ts +20 -6
  53. package/src/store/interface.ts +6 -2
@@ -330,8 +330,8 @@ describe("oauthapp domain (encryption enabled)", () => {
330
330
 
331
331
  it("blocks through the unique-slug fallback (ref pinned to a foreign org)", async () => {
332
332
  const app = await ts.command.create(appInput());
333
- // The seedpack posture: ref pinned to `org: stigmer`, app applied in
334
- // the user's own org — resolution reaches it via unique slug (#584).
333
+ // A public server's posture: ref pinned to `org: stigmer`, app applied
334
+ // in the user's own org — resolution reaches it via unique slug (#584).
335
335
  await seedReferencingMcpServer(
336
336
  ts.server.store,
337
337
  "mcps_01reffallback",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pins the OAuthApp ref-resolution ladder against Go's
3
3
  * refresolution_test.go table: exact (org, slug) wins; a UNIQUE slug-only
4
- * match is honored across orgs (the seedpack `org: stigmer` case, #584);
4
+ * match is honored across orgs (a public server's `org: stigmer` ref, #584);
5
5
  * ambiguity resolves to nothing; an empty slug is the DCR/manual-token
6
6
  * arm, not a lookup.
7
7
  */
@@ -58,7 +58,7 @@ describe("resolveOAuthAppRef", () => {
58
58
  expect(app?.metadata?.id).toBe("oaa_github_beta");
59
59
  });
60
60
 
61
- it("honors a UNIQUE slug-only match when the ref's org has no app (the seedpack case, #584)", async () => {
61
+ it("honors a UNIQUE slug-only match when the ref's org has no app (a public server's ref, #584)", async () => {
62
62
  // Two orgs hold 'github' and neither is 'stigmer': ambiguous → nothing.
63
63
  expect(await resolveOAuthAppRef(store, ref("stigmer", "github"), silentLogger)).toBeUndefined();
64
64
 
@@ -18,7 +18,7 @@
18
18
  * 1. An exact (org, slug) match wins — unique by the create pipeline's
19
19
  * duplicate check.
20
20
  * 2. Otherwise a slug-only match is honored when it is UNIQUE. This lets
21
- * a self-hosted deployment satisfy seedpack refs pinned to
21
+ * a deployment satisfy a public server's ref pinned to
22
22
  * `org: stigmer` with an OAuthApp applied in the user's own org (#584).
23
23
  * 3. Two or more slug matches with no exact hit resolve to nothing, with
24
24
  * a WARN naming the candidate orgs: ambiguity is never silently
@@ -18,7 +18,7 @@
18
18
  * 1. An exact (org, slug) match wins. Uniqueness is guaranteed by the
19
19
  * create pipeline's duplicate check.
20
20
  * 2. Otherwise a slug-only match is honored when it is UNIQUE — what
21
- * lets a self-hosted deployment satisfy seedpack refs pinned to
21
+ * lets a deployment satisfy a public server's ref pinned to
22
22
  * `org: stigmer` with an OAuthApp applied in the user's own org.
23
23
  * 3. Two or more slug matches with no exact hit resolve to nothing, with
24
24
  * a WARN naming the candidate orgs: ambiguity is never silently
@@ -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
+ });