@stigmer/server 3.15.3-dev.20260916211208 → 3.16.1-dev.20260917224426

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 (170) hide show
  1. package/dist/authorization/lane-admission.d.ts +50 -0
  2. package/dist/authorization/lane-admission.d.ts.map +1 -0
  3. package/dist/authorization/lane-admission.js +13 -0
  4. package/dist/authorization/lane-admission.js.map +1 -0
  5. package/dist/authorization/schedule-fire-caller.d.ts +2 -2
  6. package/dist/authorization/schedule-fire-caller.d.ts.map +1 -1
  7. package/dist/authorization/schedule-fire-caller.js +8 -8
  8. package/dist/authorization/schedule-fire-caller.js.map +1 -1
  9. package/dist/boot/compose.d.ts +12 -0
  10. package/dist/boot/compose.d.ts.map +1 -1
  11. package/dist/boot/compose.js +68 -14
  12. package/dist/boot/compose.js.map +1 -1
  13. package/dist/boot/inprocess.d.ts.map +1 -1
  14. package/dist/boot/inprocess.js +6 -1
  15. package/dist/boot/inprocess.js.map +1 -1
  16. package/dist/domain/executioncontext/resolve-values-for-caller.d.ts +33 -14
  17. package/dist/domain/executioncontext/resolve-values-for-caller.d.ts.map +1 -1
  18. package/dist/domain/executioncontext/resolve-values-for-caller.js +32 -8
  19. package/dist/domain/executioncontext/resolve-values-for-caller.js.map +1 -1
  20. package/dist/domain/identityaccount/actor.d.ts +33 -18
  21. package/dist/domain/identityaccount/actor.d.ts.map +1 -1
  22. package/dist/domain/identityaccount/actor.js +20 -3
  23. package/dist/domain/identityaccount/actor.js.map +1 -1
  24. package/dist/domain/identityaccount/resolve.d.ts +19 -0
  25. package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
  26. package/dist/domain/identityaccount/resolve.js +12 -0
  27. package/dist/domain/identityaccount/resolve.js.map +1 -1
  28. package/dist/domain/mcpserver/connect-execution-id.d.ts +5 -0
  29. package/dist/domain/mcpserver/connect-execution-id.d.ts.map +1 -0
  30. package/dist/domain/mcpserver/connect-execution-id.js +32 -0
  31. package/dist/domain/mcpserver/connect-execution-id.js.map +1 -0
  32. package/dist/domain/mcpserver/connect.d.ts +28 -2
  33. package/dist/domain/mcpserver/connect.d.ts.map +1 -1
  34. package/dist/domain/mcpserver/connect.js +27 -7
  35. package/dist/domain/mcpserver/connect.js.map +1 -1
  36. package/dist/domain/mcpserver/engine.d.ts +9 -3
  37. package/dist/domain/mcpserver/engine.d.ts.map +1 -1
  38. package/dist/domain/mcpserver/engine.js.map +1 -1
  39. package/dist/domain/mcpserver/start-connect.js +1 -1
  40. package/dist/domain/mcpserver/start-connect.js.map +1 -1
  41. package/dist/domain/platform/controller.d.ts +31 -11
  42. package/dist/domain/platform/controller.d.ts.map +1 -1
  43. package/dist/domain/platform/controller.js +2 -3
  44. package/dist/domain/platform/controller.js.map +1 -1
  45. package/dist/domain/plugin/materialize/agent.d.ts +10 -1
  46. package/dist/domain/plugin/materialize/agent.d.ts.map +1 -1
  47. package/dist/domain/plugin/materialize/agent.js +35 -1
  48. package/dist/domain/plugin/materialize/agent.js.map +1 -1
  49. package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
  50. package/dist/domain/plugin/materialize/plan.js +1 -1
  51. package/dist/domain/plugin/materialize/plan.js.map +1 -1
  52. package/dist/domain/plugin/overlay/sanitize.d.ts.map +1 -1
  53. package/dist/domain/plugin/overlay/sanitize.js +4 -2
  54. package/dist/domain/plugin/overlay/sanitize.js.map +1 -1
  55. package/dist/domain/project/reconcile.d.ts.map +1 -1
  56. package/dist/domain/project/reconcile.js +14 -10
  57. package/dist/domain/project/reconcile.js.map +1 -1
  58. package/dist/domain/workflowexecution/phases.d.ts +21 -0
  59. package/dist/domain/workflowexecution/phases.d.ts.map +1 -0
  60. package/dist/domain/workflowexecution/phases.js +31 -0
  61. package/dist/domain/workflowexecution/phases.js.map +1 -0
  62. package/dist/identity/oidc-verifier.js +8 -1
  63. package/dist/identity/oidc-verifier.js.map +1 -1
  64. package/dist/pipeline/apiresource-meta.d.ts +15 -0
  65. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  66. package/dist/pipeline/apiresource-meta.js +34 -0
  67. package/dist/pipeline/apiresource-meta.js.map +1 -1
  68. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  69. package/dist/pipeline/steps/guard-memory-capture.js +5 -3
  70. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  71. package/dist/runnerauth/bound-execution.d.ts +27 -0
  72. package/dist/runnerauth/bound-execution.d.ts.map +1 -0
  73. package/dist/runnerauth/bound-execution.js +137 -0
  74. package/dist/runnerauth/bound-execution.js.map +1 -0
  75. package/dist/runnerauth/built-in-runner-credential-provider.d.ts +28 -0
  76. package/dist/runnerauth/built-in-runner-credential-provider.d.ts.map +1 -0
  77. package/dist/runnerauth/built-in-runner-credential-provider.js +217 -0
  78. package/dist/runnerauth/built-in-runner-credential-provider.js.map +1 -0
  79. package/dist/runnerauth/constants.d.ts +63 -0
  80. package/dist/runnerauth/constants.d.ts.map +1 -0
  81. package/dist/runnerauth/constants.js +63 -0
  82. package/dist/runnerauth/constants.js.map +1 -0
  83. package/dist/runnerauth/dispatch-credential.d.ts +38 -0
  84. package/dist/runnerauth/dispatch-credential.d.ts.map +1 -0
  85. package/dist/runnerauth/dispatch-credential.js +17 -0
  86. package/dist/runnerauth/dispatch-credential.js.map +1 -0
  87. package/dist/runnerauth/runner-credential-provider.d.ts +49 -11
  88. package/dist/runnerauth/runner-credential-provider.d.ts.map +1 -1
  89. package/dist/runnerauth/runner-credential-provider.js +16 -4
  90. package/dist/runnerauth/runner-credential-provider.js.map +1 -1
  91. package/dist/runnerauth/runner-subject-verifier.d.ts +14 -0
  92. package/dist/runnerauth/runner-subject-verifier.d.ts.map +1 -0
  93. package/dist/runnerauth/runner-subject-verifier.js +115 -0
  94. package/dist/runnerauth/runner-subject-verifier.js.map +1 -0
  95. package/dist/runnerauth/runnerauth.d.ts +33 -4
  96. package/dist/runnerauth/runnerauth.d.ts.map +1 -1
  97. package/dist/runnerauth/runnerauth.js +131 -35
  98. package/dist/runnerauth/runnerauth.js.map +1 -1
  99. package/dist/temporal/agentexecution/engine-client.d.ts +13 -0
  100. package/dist/temporal/agentexecution/engine-client.d.ts.map +1 -1
  101. package/dist/temporal/agentexecution/engine-client.js +10 -2
  102. package/dist/temporal/agentexecution/engine-client.js.map +1 -1
  103. package/dist/temporal/agentexecution/workflow-input.d.ts +13 -0
  104. package/dist/temporal/agentexecution/workflow-input.d.ts.map +1 -1
  105. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
  106. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +35 -31
  107. package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
  108. package/dist/temporal/workflowexecution/engine-client.d.ts +24 -0
  109. package/dist/temporal/workflowexecution/engine-client.d.ts.map +1 -1
  110. package/dist/temporal/workflowexecution/engine-client.js +26 -19
  111. package/dist/temporal/workflowexecution/engine-client.js.map +1 -1
  112. package/dist/temporal/workflowexecution/workflow-input.d.ts +14 -0
  113. package/dist/temporal/workflowexecution/workflow-input.d.ts.map +1 -1
  114. package/package.json +5 -5
  115. package/src/authorization/__tests__/lane-admission.test.ts +115 -0
  116. package/src/authorization/lane-admission.ts +73 -0
  117. package/src/authorization/schedule-fire-caller.ts +10 -14
  118. package/src/boot/compose.ts +93 -22
  119. package/src/boot/inprocess.ts +7 -1
  120. package/src/domain/executioncontext/__tests__/executioncontext.test.ts +104 -0
  121. package/src/domain/executioncontext/__tests__/resolve-values-capability.test.ts +33 -4
  122. package/src/domain/executioncontext/resolve-values-for-caller.ts +79 -22
  123. package/src/domain/identityaccount/__tests__/account-for-stamp.test.ts +111 -0
  124. package/src/domain/identityaccount/actor.ts +61 -21
  125. package/src/domain/identityaccount/resolve.ts +31 -0
  126. package/src/domain/mcpserver/__tests__/connect.test.ts +17 -2
  127. package/src/domain/mcpserver/connect-execution-id.ts +34 -0
  128. package/src/domain/mcpserver/connect.ts +38 -3
  129. package/src/domain/mcpserver/engine.ts +9 -3
  130. package/src/domain/mcpserver/start-connect.ts +1 -1
  131. package/src/domain/platform/__tests__/platform.test.ts +47 -1
  132. package/src/domain/platform/controller.ts +33 -14
  133. package/src/domain/plugin/__tests__/materialize.test.ts +60 -1
  134. package/src/domain/plugin/__tests__/plugin.test.ts +122 -1
  135. package/src/domain/plugin/__tests__/sanitize.test.ts +2 -2
  136. package/src/domain/plugin/materialize/agent.ts +38 -0
  137. package/src/domain/plugin/materialize/plan.ts +7 -1
  138. package/src/domain/plugin/overlay/sanitize.ts +4 -2
  139. package/src/domain/project/__tests__/reconcile.test.ts +51 -5
  140. package/src/domain/project/reconcile.ts +20 -15
  141. package/src/domain/workflowexecution/__tests__/phases.test.ts +58 -0
  142. package/src/domain/workflowexecution/phases.ts +33 -0
  143. package/src/extensions/__tests__/extension-composition.test.ts +7 -1
  144. package/src/identity/__tests__/oidc-verifier.test.ts +22 -0
  145. package/src/identity/oidc-verifier.ts +8 -1
  146. package/src/pipeline/__tests__/kind-by-id-prefix.test.ts +75 -0
  147. package/src/pipeline/apiresource-meta.ts +41 -0
  148. package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +55 -35
  149. package/src/pipeline/steps/guard-memory-capture.ts +5 -3
  150. package/src/runnerauth/__tests__/bound-execution.test.ts +334 -0
  151. package/src/runnerauth/__tests__/built-in-runner-credential-provider.test.ts +516 -0
  152. package/src/runnerauth/__tests__/dispatch-credential.test.ts +94 -0
  153. package/src/runnerauth/__tests__/runner-credential-provider.test.ts +27 -0
  154. package/src/runnerauth/__tests__/runner-subject-composed.test.ts +497 -0
  155. package/src/runnerauth/__tests__/runner-subject-verifier.test.ts +541 -0
  156. package/src/runnerauth/bound-execution.ts +270 -0
  157. package/src/runnerauth/built-in-runner-credential-provider.ts +300 -0
  158. package/src/runnerauth/constants.ts +74 -0
  159. package/src/runnerauth/dispatch-credential.ts +63 -0
  160. package/src/runnerauth/runner-credential-provider.ts +60 -11
  161. package/src/runnerauth/runner-subject-verifier.ts +152 -0
  162. package/src/runnerauth/runnerauth.ts +148 -38
  163. package/src/temporal/agentexecution/__tests__/engine-client.test.ts +107 -38
  164. package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +105 -2
  165. package/src/temporal/agentexecution/engine-client.ts +27 -2
  166. package/src/temporal/agentexecution/workflow-input.ts +13 -0
  167. package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +90 -44
  168. package/src/temporal/workflowexecution/__tests__/engine-client.test.ts +139 -33
  169. package/src/temporal/workflowexecution/engine-client.ts +48 -21
  170. package/src/temporal/workflowexecution/workflow-input.ts +14 -0
@@ -118,7 +118,7 @@ export async function startConnect(
118
118
  }
119
119
  }
120
120
 
121
- const prepared = await prepareConnect(deps, mcpServer, input);
121
+ const prepared = await prepareConnect(deps, mcpServer, input, identity);
122
122
 
123
123
  // Taken before the start so the advisory describes the queue the run is
124
124
  // about to join. Warn-only by design: a worker may be booting (the
@@ -14,7 +14,10 @@
14
14
  * executioncontext decrypt lane uses, bound to exactly the named
15
15
  * execution id;
16
16
  * - getRunnerBootstrapConfig echoes the configured Temporal coordinates
17
- * with the token fields empty (minting a proxy token is cloud-only).
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
  */
19
22
  import { mkdtempSync, rmSync } from "node:fs";
20
23
  import { tmpdir } from "node:os";
@@ -178,6 +181,48 @@ describe("platform domain (composed server)", () => {
178
181
  });
179
182
  });
180
183
 
184
+ describe("platform domain (composed with a stated version)", () => {
185
+ let server: ComposedServer;
186
+ let client: PlatformClient;
187
+ let dir: string;
188
+
189
+ beforeAll(async () => {
190
+ dir = mkdtempSync(path.join(tmpdir(), "platform-domain-version-test-"));
191
+ vi.stubEnv("STIGMER_ENCRYPTION_KEY", ENC_KEY.toString("base64"));
192
+ server = await composeServer({
193
+ config: loadConfig({
194
+ STIGMER_MODEL_REGISTRY_REFRESH: "off",
195
+ DB_PATH: path.join(dir, "stigmer.db"),
196
+ ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
197
+ }),
198
+ logger: silentLogger,
199
+ version: "3.99.0",
200
+ portOverride: 0,
201
+ host: "127.0.0.1",
202
+ });
203
+ const port = await server.start();
204
+ client = createClient(
205
+ PlatformQueryController,
206
+ createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` }),
207
+ );
208
+ });
209
+
210
+ afterAll(async () => {
211
+ await server.shutdown();
212
+ rmSync(dir, { recursive: true, force: true });
213
+ vi.unstubAllEnvs();
214
+ });
215
+
216
+ it("getServerInfo reports the composition's stated release, not the build stamp", async () => {
217
+ // A library consumer (the cloud composition) runs no bundle, so the
218
+ // define is never stamped for it; ComposeOptions.version is how it
219
+ // states the release it installed. Edition is untouched by it.
220
+ const info = await client.getServerInfo({});
221
+ expect(info.version).toBe("3.99.0");
222
+ expect(info.edition).toBe(ServerEdition.oss);
223
+ });
224
+ });
225
+
181
226
  describe("platform domain (keyless runner-token service)", () => {
182
227
  it("a keyless service answers not-minted — fail-soft, never an error", async () => {
183
228
  const transport = createRouterTransport((router) => {
@@ -188,6 +233,7 @@ describe("platform domain (keyless runner-token service)", () => {
188
233
  RunnerAuthService.create(undefined),
189
234
  ),
190
235
  edition: ServerEdition.oss,
236
+ version: "dev",
191
237
  logger: silentLogger,
192
238
  });
193
239
  });
@@ -10,21 +10,34 @@
10
10
  * composed-server pattern).
11
11
  *
12
12
  * getRunnerScopedToken is the mint side of the runner-token lane
13
- * (oss#535) — the seam src/runnerauth/ reserved for this sub-project. OSS
14
- * has no caller identity, so unlike the cloud exchange there is no
15
- * credential to verify: any caller naming an execution receives a token
16
- * for it. On a single-user server the token is the LANE DISCRIMINATOR that
17
- * lets the EC read RPCs redact by default without breaking the runner, not
18
- * a trust boundary (DD-004).
13
+ * (oss#535) — the seam src/runnerauth/. What the token IS depends on the
14
+ * server's posture, and so does who may obtain one:
15
+ *
16
+ * - Under trusted-local (the laptop: one caller, nothing to separate)
17
+ * the arms below mint a clocked token for any caller naming an
18
+ * execution. The token is the LANE DISCRIMINATOR that lets the
19
+ * ExecutionContext read RPCs redact by default without breaking the
20
+ * runner, not a trust boundary — there is no identity for it to
21
+ * change.
22
+ * - Under the built-in authorization posture the same token is an
23
+ * IDENTITY: the runner-subject verifier admits its bearer as the
24
+ * human whose run it is (runnerauth/runner-subject-verifier.ts). A
25
+ * mint for anyone would be impersonation, so the built-in provider
26
+ * defines exchangeScopedToken and this controller delegates to it:
27
+ * the run credential is minted for the run's own person and nobody
28
+ * else (runnerauth/built-in-runner-credential-provider.ts). Runs get
29
+ * their credential from the dispatch itself (the engine clients put
30
+ * it on the workflow input); this exchange is the runner's fallback.
19
31
  *
20
32
  * The C4 capability delegation (20260827.09, gate ruling Q1): when the
21
33
  * composed provider defines exchangeScopedToken / bootstrapCredentials,
22
34
  * this controller delegates the exchange arms and the bootstrap
23
- * credential fields to it wholesale — the cloud's per-arm caller-class
24
- * gating, row authorization, and multi-lane minting are POLICY, not
25
- * contract, and live behind the seam. With neither defined (the OSS
26
- * default), every arm below behaves exactly as before; the conformance
27
- * suites pin that byte-identity.
35
+ * credential fields to it wholesale — per-arm caller gating, row
36
+ * authorization, and multi-lane minting are POLICY, not contract, and
37
+ * live behind the seam (the cloud's four-arm exchange; open source's
38
+ * built-in gate). With neither defined (the trusted-local default), every
39
+ * arm below behaves exactly as before; the conformance suites pin that
40
+ * byte-identity.
28
41
  */
29
42
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
30
43
  import { create } from "@bufbuild/protobuf";
@@ -50,7 +63,6 @@ import type {
50
63
  RunnerScopedTokenRequest,
51
64
  } from "../../runnerauth/runner-credential-provider.js";
52
65
  import { TOKEN_TYPE_EXECUTION_SCOPED } from "../../runnerauth/runnerauth.js";
53
- import { SERVER_VERSION } from "./version.js";
54
66
 
55
67
  export interface PlatformControllerDeps {
56
68
  /**
@@ -75,6 +87,13 @@ export interface PlatformControllerDeps {
75
87
  * cloud composition answers `cloud` without forking this controller.
76
88
  */
77
89
  readonly edition: ServerEdition;
90
+ /**
91
+ * The release reported beside the edition. The composition root resolves
92
+ * it (ComposeOptions.version, else SERVER_VERSION), so this controller
93
+ * never reads the build stamp itself: a library consumer with no bundle
94
+ * states its installed release and answers exactly as a bundle would.
95
+ */
96
+ readonly version: string;
78
97
  readonly logger: Logger;
79
98
  }
80
99
 
@@ -92,11 +111,11 @@ export function registerPlatformServices(
92
111
  });
93
112
  }
94
113
 
95
- /** Go GetServerInfo: the server edition and build version. */
114
+ /** Go GetServerInfo: the server edition and release version. */
96
115
  function getServerInfo(deps: PlatformControllerDeps): GetServerInfoOutput {
97
116
  return create(GetServerInfoOutputSchema, {
98
117
  edition: deps.edition,
99
- version: SERVER_VERSION,
118
+ version: deps.version,
100
119
  });
101
120
  }
102
121
 
@@ -7,7 +7,9 @@
7
7
  * agent, the `mcpServers` key-to-slug rule, a skill archive rooted at its
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
- * transport refused, and the convergence and drop rules over members.
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
13
  */
12
14
  import { describe, expect, it } from "vitest";
13
15
 
@@ -19,8 +21,11 @@ import type { PluginPackage } from "@stigmer/plugin-package";
19
21
  import { cursorPlugin, openPlugin } from "@stigmer/plugin-package/testing";
20
22
  import type { PluginFixture } from "@stigmer/plugin-package/testing";
21
23
  import { create } from "@bufbuild/protobuf";
24
+ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
25
+ import { AgentSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
22
26
  import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
23
27
  import {
28
+ McpServerAuthSchema,
24
29
  McpServerSpecSchema,
25
30
  StdioServerConfigSchema,
26
31
  } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
@@ -224,6 +229,60 @@ describe("planMaterialization", () => {
224
229
  expect(builtPlan.skills[0]!.request.tag).toBe("");
225
230
  });
226
231
 
232
+ it("declares the servers' variables on the composed agent so a session asks for them", () => {
233
+ expect(plan.agent!.resource.spec?.env).toEqual({
234
+ GITHUB_TOKEN: expect.objectContaining({ isSecret: true, optional: false }),
235
+ });
236
+ });
237
+
238
+ it("leaves an OAuth-managed target out of the agent's declarations", () => {
239
+ const overlay = create(McpServerSchema, {
240
+ spec: create(McpServerSpecSchema, {
241
+ auth: create(McpServerAuthSchema, { targetEnvVar: "GITHUB_TOKEN" }),
242
+ }),
243
+ });
244
+ const withOAuth = planMaterialization(
245
+ pkg,
246
+ inMemoryPluginFiles(thermos),
247
+ {
248
+ workflows: [],
249
+ mcpServers: [
250
+ {
251
+ path: "ai.stigmer/mcp-servers/my_github.yaml",
252
+ server: "my_github",
253
+ resource: overlay,
254
+ },
255
+ ],
256
+ },
257
+ IDENTITY,
258
+ "",
259
+ );
260
+ // The server still declares it (the connect flow reads it there); the
261
+ // agent does not, because the value comes from a managed environment.
262
+ expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty("GITHUB_TOKEN");
263
+ expect(withOAuth.agent!.resource.spec?.env).toEqual({});
264
+ });
265
+
266
+ it("takes an authored agent as written, declarations included", () => {
267
+ const authored = create(AgentSchema, {
268
+ spec: create(AgentSpecSchema, {
269
+ instructions: "The author's own instructions for this agent.",
270
+ }),
271
+ });
272
+ const withOverlay = planMaterialization(
273
+ pkg,
274
+ inMemoryPluginFiles(thermos),
275
+ {
276
+ agent: { path: "ai.stigmer/agent.yaml", resource: authored },
277
+ workflows: [],
278
+ mcpServers: [],
279
+ },
280
+ IDENTITY,
281
+ "",
282
+ );
283
+ expect(withOverlay.agent!.resource.spec?.env).toEqual({});
284
+ });
285
+
227
286
  it("refuses a server overlay that redefines the transport", () => {
228
287
  const overlay = create(McpServerSchema, {
229
288
  spec: create(McpServerSpecSchema, {
@@ -10,7 +10,9 @@
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
12
  * unmanaged resource holds refuses the install; a bad overlay refuses
13
- * before any write.
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.
14
16
  */
15
17
  import { mkdtempSync, rmSync } from "node:fs";
16
18
  import net from "node:net";
@@ -46,11 +48,13 @@ import { SkillQueryController } from "@stigmer/protos/ai/stigmer/agentic/skill/v
46
48
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
47
49
  import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
48
50
  import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
51
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
49
52
 
50
53
  import { writeArchive } from "../../../archive/write.js";
51
54
  import { loadConfig } from "../../../boot/config.js";
52
55
  import { composeServer } from "../../../boot/compose.js";
53
56
  import type { ComposedServer } from "../../../boot/compose.js";
57
+ import type { Authorizer, AuthzCheck } from "../../../extensions/authorizer.js";
54
58
  import { createLogger } from "../../../boot/logger.js";
55
59
  import {
56
60
  PLUGIN_LABEL,
@@ -292,6 +296,12 @@ describe("Plugin push — materialisation", () => {
292
296
  isSecret: true,
293
297
  optional: false,
294
298
  });
299
+ // The agent declares the same variable, which is what makes a session
300
+ // ask for it and the execution's least-privilege filter pass it.
301
+ expect(agent.spec?.env["GITHUB_TOKEN"]).toMatchObject({
302
+ isSecret: true,
303
+ optional: false,
304
+ });
295
305
  });
296
306
 
297
307
  it("installs an MCP-only plugin as its servers and no agent", async () => {
@@ -679,3 +689,114 @@ describe("Plugin push — refusals before any write", () => {
679
689
  expect(agent.metadata?.labels[PLUGIN_LABEL]).toBe(installed.metadata?.id);
680
690
  });
681
691
  });
692
+
693
+ /**
694
+ * The reserved-label refusal where an authorizer enforces. The open-source
695
+ * built-in authorizer allows `can_write_reserved_labels` by design, so the
696
+ * default composition above cannot show the refusal; this composition
697
+ * registers an authorizer that denies exactly that permission and allows
698
+ * everything else, the posture a hosted edition takes. An archive whose
699
+ * overlay carries a `stigmer.ai/*` label is refused before a byte is
700
+ * written, and a plain overlay under the same authorizer still installs —
701
+ * so the sanitiser asks the platform capability and nothing broader.
702
+ */
703
+ describe("Plugin push under an authorizer that enforces reserved labels", () => {
704
+ let enforcingDir: string;
705
+ let enforcing: ComposedServer;
706
+ let enforcingPlugins: Client<typeof PluginCommandController>;
707
+ let enforcingQuery: Client<typeof PluginQueryController>;
708
+ const observed: AuthzCheck[] = [];
709
+
710
+ beforeAll(async () => {
711
+ enforcingDir = mkdtempSync(path.join(tmpdir(), "plugin-domain-enforcing-"));
712
+ const grpcPort = await reserveFreePort();
713
+ const authorizer: Authorizer = {
714
+ authorize(_caller, check) {
715
+ if (check.permission === IamPermission.can_write_reserved_labels) {
716
+ observed.push(check);
717
+ return Promise.resolve({
718
+ kind: "deny",
719
+ reason: "reserved labels are the platform's",
720
+ });
721
+ }
722
+ return Promise.resolve({ kind: "allow" });
723
+ },
724
+ };
725
+ enforcing = await composeServer({
726
+ config: loadConfig({
727
+ STIGMER_MODEL_REGISTRY_REFRESH: "off",
728
+ TEMPORAL_HOST_PORT: "127.0.0.1:1",
729
+ GRPC_PORT: String(grpcPort),
730
+ DB_PATH: path.join(enforcingDir, "stigmer.db"),
731
+ ARTIFACT_LOCAL_BASE_PATH: path.join(enforcingDir, "artifacts"),
732
+ STORAGE_PATH: path.join(enforcingDir, "storage"),
733
+ }),
734
+ logger: silentLogger,
735
+ host: "127.0.0.1",
736
+ extensions: [{ name: "reserved-labels-enforced", authorizer }],
737
+ });
738
+ const port = await enforcing.start();
739
+ const transport: Transport = createGrpcTransport({
740
+ baseUrl: `http://127.0.0.1:${port}`,
741
+ });
742
+ enforcingPlugins = createClient(PluginCommandController, transport);
743
+ enforcingQuery = createClient(PluginQueryController, transport);
744
+ });
745
+
746
+ afterAll(async () => {
747
+ await enforcing.shutdown();
748
+ rmSync(enforcingDir, { recursive: true, force: true });
749
+ });
750
+
751
+ it("refuses an archive whose overlay carries a reserved label, naming the document and the key, and writes nothing", async () => {
752
+ const name = uniqueName("plg-forged");
753
+ const forged = withFile(
754
+ thermosLike(name),
755
+ "ai.stigmer/agent.yaml",
756
+ [
757
+ "apiVersion: agentic.stigmer.ai/v1",
758
+ "kind: Agent",
759
+ "metadata:",
760
+ ` name: ${name}`,
761
+ " labels:",
762
+ " stigmer.ai/plugin: plg_somebody_else",
763
+ "spec:",
764
+ " instructions: Long enough instructions for the forged agent.",
765
+ "",
766
+ ].join("\n"),
767
+ );
768
+ const error = await expectCode(
769
+ enforcingPlugins.push({ org: ORG, artifact: archiveOf(forged) }),
770
+ Code.InvalidArgument,
771
+ );
772
+ expect(error.rawMessage).toContain(
773
+ "cannot be set by a plugin (ai.stigmer/agent.yaml: stigmer.ai/plugin)",
774
+ );
775
+ expect(observed.map((c) => c.permission)).toEqual([
776
+ IamPermission.can_write_reserved_labels,
777
+ ]);
778
+ await expectCode(
779
+ enforcingQuery.getByReference(
780
+ createMessage(ApiResourceReferenceSchema, {
781
+ org: ORG,
782
+ kind: ApiResourceKind.plugin,
783
+ slug: name,
784
+ }),
785
+ ),
786
+ Code.NotFound,
787
+ );
788
+ });
789
+
790
+ it("installs a plain overlay under the same authorizer: the platform stamps its own labels through the in-process origin", async () => {
791
+ const name = uniqueName("plg-honest");
792
+ const installed = await enforcingPlugins.push({
793
+ org: ORG,
794
+ artifact: archiveOf(thermosLike(name)),
795
+ });
796
+ expect(installed.status?.state).toBe(PluginState.READY);
797
+ const { members } = await enforcingQuery.listMembers({
798
+ value: installed.metadata!.id,
799
+ });
800
+ expect(members).toHaveLength(3);
801
+ });
802
+ });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pins the sanitiser, the one place plugin-provided metadata is judged
3
3
  * before it rides the in-process lane: a `stigmer.ai/*` label in an
4
- * overlay is refused PERMISSION_DENIED naming the document and the keys
4
+ * overlay is refused INVALID_ARGUMENT naming the document and the keys
5
5
  * under a denying authorizer and passes under an allowing one (the
6
6
  * open-source posture), the operator check is consulted lazily and only
7
7
  * for the platform capability, a preset id and a name that describes
@@ -86,7 +86,7 @@ describe("sanitizeOverlays", () => {
86
86
  USER,
87
87
  ),
88
88
  );
89
- expect(error.code).toBe(Code.PermissionDenied);
89
+ expect(error.code).toBe(Code.InvalidArgument);
90
90
  expect(error.rawMessage).toContain(
91
91
  "ai.stigmer/agent.yaml: stigmer.ai/default-agent",
92
92
  );
@@ -14,6 +14,18 @@
14
14
  * `mcp_server_usage`, every `agents/*.md` as a `sub_agent` with access to
15
15
  * all the plugin's servers and the skills it asked for.
16
16
  *
17
+ * The composed agent declares in `env` the union of its servers' variables,
18
+ * OAuth-managed target variables excluded. An execution filters its
19
+ * environment to the AGENT's declared keys, and every client's session
20
+ * start asks the user for the AGENT's declared keys, so an agent declaring
21
+ * nothing over a server declaring `${API_TOKEN}` is never asked for the
22
+ * token and fails at its first tool call. The declaration on the agent is
23
+ * what routes the user's values to the execution; the declaration on the
24
+ * server stays too (the connect flow and the runner's per-server filter
25
+ * read it there). An OAuth target is excluded because its value is injected
26
+ * from a managed environment, never asked of the user. An author's
27
+ * `agent.yaml` is taken as written: they chose what to declare.
28
+ *
17
29
  * Model hints are recorded, never applied: a dialect's `model` becomes a
18
30
  * status warning and the sub-agent runs on the session's model. The
19
31
  * catalog seam exposes neither family nor cost tier, the runner drops a
@@ -25,6 +37,7 @@
25
37
  import { create } from "@bufbuild/protobuf";
26
38
 
27
39
  import type { PluginPackage, PluginSubAgent } from "@stigmer/plugin-package";
40
+ import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
28
41
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
29
42
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
30
43
  import {
@@ -47,6 +60,7 @@ import { renderDefaultAgentInstructions } from "./default-agent-instructions.js"
47
60
  import { memberMetadata } from "./identity.js";
48
61
  import type { PluginIdentity } from "./identity.js";
49
62
  import { mcpServerSlugOf } from "./mcp-servers.js";
63
+ import type { PlannedMcpServer } from "./mcp-servers.js";
50
64
  import { skillSlugOf } from "./skills.js";
51
65
 
52
66
  export interface PlannedAgent {
@@ -67,6 +81,7 @@ export function carriesAgent(plugin: PluginPackage): boolean {
67
81
  export function planAgent(
68
82
  plugin: PluginPackage,
69
83
  overlayAgent: Agent | undefined,
84
+ mcpServers: readonly PlannedMcpServer[],
70
85
  identity: PluginIdentity,
71
86
  warnings: PluginWarning[],
72
87
  ): PlannedAgent | undefined {
@@ -95,6 +110,7 @@ export function planAgent(
95
110
  const spec = create(AgentSpecSchema, {
96
111
  description: plugin.description ?? "",
97
112
  instructions: renderDefaultAgentInstructions(plugin),
113
+ env: toolVariables(mcpServers),
98
114
  skillRefs: [...skillSlugByName.values()].map((slug) =>
99
115
  create(ApiResourceReferenceSchema, {
100
116
  org: identity.org,
@@ -127,6 +143,28 @@ export function planAgent(
127
143
  };
128
144
  }
129
145
 
146
+ /**
147
+ * The variables the agent's tools need from the user: every server's
148
+ * declarations, minus each server's OAuth target. Two servers declaring
149
+ * one name share one declaration; the first server's wins, and the
150
+ * library already refuses a plugin whose variables disagree about a name.
151
+ */
152
+ export function toolVariables(
153
+ mcpServers: readonly PlannedMcpServer[],
154
+ ): Record<string, EnvVarDeclaration> {
155
+ const declarations: Record<string, EnvVarDeclaration> = {};
156
+ for (const server of mcpServers) {
157
+ const spec = server.resource.spec;
158
+ if (spec === undefined) continue;
159
+ const oauthTarget = spec.auth?.targetEnvVar ?? "";
160
+ for (const [name, declaration] of Object.entries(spec.env)) {
161
+ if (name === oauthTarget || name in declarations) continue;
162
+ declarations[name] = declaration;
163
+ }
164
+ }
165
+ return declarations;
166
+ }
167
+
130
168
  function planSubAgent(
131
169
  subAgent: PluginSubAgent,
132
170
  identity: PluginIdentity,
@@ -96,7 +96,13 @@ export function planMaterialization(
96
96
  ),
97
97
  identity,
98
98
  );
99
- const agent = planAgent(plugin, overlays.agent?.resource, identity, warnings);
99
+ const agent = planAgent(
100
+ plugin,
101
+ overlays.agent?.resource,
102
+ mcpServers,
103
+ identity,
104
+ warnings,
105
+ );
100
106
  const workflows = planWorkflows(overlays.workflows, identity);
101
107
 
102
108
  const members: PlannedMember[] = [
@@ -13,7 +13,9 @@
13
13
  * `platform:stigmer`, the same check GuardReservedLabels runs and the
14
14
  * same lazy consultation of the one composed Authorizer (the
15
15
  * open-source permissive default allows; an enforcing authorizer
16
- * denies with PERMISSION_DENIED naming the keys);
16
+ * denies with INVALID_ARGUMENT naming the keys, the code the
17
+ * reserved-label guard answers a client mutation with, so one refusal
18
+ * class carries one code wherever it is met);
17
19
  * - `metadata.id` is the server's to mint, never an author's;
18
20
  * - the agent overlay's name is the plugin's, a server overlay's name is
19
21
  * its `mcpServers` key, a workflow overlay's name is its file stem: an
@@ -130,7 +132,7 @@ export async function sanitizeOverlays(
130
132
  throw new ConnectError(
131
133
  `Labels in the reserved '${RESERVED_LABEL_PREFIX}' namespace are platform-managed and ` +
132
134
  `cannot be set by a plugin (${listed}). Remove them from the plugin's ai.stigmer/ documents.`,
133
- Code.PermissionDenied,
135
+ Code.InvalidArgument,
134
136
  );
135
137
  }
136
138
  case "unavailable":
@@ -237,10 +237,12 @@ describe("orphan deletion (Go service_test.go)", () => {
237
237
  ]);
238
238
  });
239
239
 
240
- it("orphan lookup is slug-only, matching across orgs (pinned Go behavior)", async () => {
241
- // Seeded under org "local"; the orphan reference carries a DIFFERENT
242
- // org — resolution still matches, because Go's resolveResourceID
243
- // filters by slug alone (reconcile/service.go:126).
240
+ it("orphan lookup is scoped to the reference's org: another org's same-slug resource is never touched", async () => {
241
+ // Slugs are org-scoped identifiers. Seeded under org "local"; the orphan
242
+ // reference names a DIFFERENT org, so it must not resolve to this row.
243
+ // (The Go port matched by slug alone; on a backend with a system org
244
+ // beside the user's, a prune could then delete the wrong org's
245
+ // resource — stigmer/stigmer#1151.)
244
246
  await seedAgent("agent-id-x", "cross-org-agent");
245
247
  const deleter = new RecordingDeleter();
246
248
  const previous = [
@@ -251,9 +253,53 @@ describe("orphan deletion (Go service_test.go)", () => {
251
253
  }),
252
254
  ];
253
255
  const result = await service(deleter).reconcile(previous, [], DEFAULT_RECONCILIATION_OPTIONS);
256
+ expect(result.removed).toHaveLength(0);
257
+ expect(result.errors).toHaveLength(1);
258
+ expect(result.errors[0]?.resourceKey).toBe("agent:cross-org-agent");
259
+ expect(result.errors[0]?.message).toBe("failed to resolve resource for deletion");
260
+ expect(deleter.calls).toHaveLength(0);
261
+ });
262
+
263
+ it("orphan lookup picks the reference's org when two orgs hold the slug", async () => {
264
+ await seedAgent("agent-id-local", "shared-slug");
265
+ await store.saveResource(
266
+ ApiResourceKind.agent,
267
+ "agent-id-other",
268
+ AgentSchema,
269
+ create(AgentSchema, {
270
+ apiVersion: "agentic.stigmer.ai/v1",
271
+ kind: "Agent",
272
+ metadata: { id: "agent-id-other", name: "shared-slug", slug: "shared-slug", org: "another-org" },
273
+ }),
274
+ );
275
+ const deleter = new RecordingDeleter();
276
+ const previous = [
277
+ create(ApiResourceReferenceSchema, {
278
+ org: "another-org",
279
+ kind: ApiResourceKind.agent,
280
+ slug: "shared-slug",
281
+ }),
282
+ ];
283
+ const result = await service(deleter).reconcile(previous, [], DEFAULT_RECONCILIATION_OPTIONS);
284
+ expect(result.removed).toHaveLength(1);
285
+ expect(deleter.calls).toEqual([
286
+ { kind: ApiResourceKind.agent, resourceId: "agent-id-other" },
287
+ ]);
288
+ });
289
+
290
+ it("a reference without an org keeps the unscoped lookup (a pre-scoping client's project)", async () => {
291
+ await seedAgent("agent-id-y", "legacy-agent");
292
+ const deleter = new RecordingDeleter();
293
+ const previous = [
294
+ create(ApiResourceReferenceSchema, {
295
+ kind: ApiResourceKind.agent,
296
+ slug: "legacy-agent",
297
+ }),
298
+ ];
299
+ const result = await service(deleter).reconcile(previous, [], DEFAULT_RECONCILIATION_OPTIONS);
254
300
  expect(result.removed).toHaveLength(1);
255
301
  expect(deleter.calls).toEqual([
256
- { kind: ApiResourceKind.agent, resourceId: "agent-id-x" },
302
+ { kind: ApiResourceKind.agent, resourceId: "agent-id-y" },
257
303
  ]);
258
304
  });
259
305
 
@@ -37,6 +37,7 @@ import { ReconciliationSummarySchema } from "@stigmer/protos/ai/stigmer/tenancy/
37
37
  import type { ReconciliationSummary } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/status_pb";
38
38
 
39
39
  import type { Logger } from "../../boot/logger.js";
40
+ import { findResourceBySlug } from "../../pipeline/steps/helpers.js";
40
41
  import { metadataOf } from "../../pipeline/steps/shapes.js";
41
42
  import type { Store } from "../../store/interface.js";
42
43
 
@@ -205,28 +206,32 @@ async function deleteOrphans(
205
206
  }
206
207
 
207
208
  /**
208
- * Resolves an orphan reference to its resource id — Go resolveResourceID:
209
- * FindByField on metadata.slug, SLUG ONLY, no org filter (the pinned
210
- * behavior: first match of that kind+slug across all orgs; tolerable in
211
- * single-tenant OSS).
209
+ * Resolves an orphan reference to its resource id, within the org the
210
+ * reference names. Slugs are org-scoped identifiers: a local backend holds
211
+ * the `stigmer` system org beside the user's, a self-host holds many, and
212
+ * the same slug may exist in several. The Go port matched by slug alone and
213
+ * could prune another org's resource (stigmer/stigmer#1151); the CLI has
214
+ * always stamped `org` on every member reference, so scoping is a change
215
+ * of lookup, not of contract. A reference without an org (a project written
216
+ * before members carried one) keeps the unscoped match, the helper's
217
+ * documented empty-org arm.
212
218
  */
213
219
  async function resolveResourceId(
214
220
  store: Store,
215
221
  ref: ApiResourceReference,
216
222
  ): Promise<string> {
217
223
  const schema = schemaForKind(ref.kind);
218
- let resource;
219
- try {
220
- resource = await store.findByField(
221
- ref.kind,
222
- "metadata.slug",
223
- ref.slug,
224
- schema,
225
- );
226
- } catch (cause) {
224
+ const resource = await findResourceBySlug(
225
+ store,
226
+ ref.kind,
227
+ schema,
228
+ ref.slug,
229
+ ref.org,
230
+ );
231
+ if (resource === undefined) {
232
+ const where = ref.org === "" ? "" : ` in org '${ref.org}'`;
227
233
  throw new Error(
228
- `resource ${kindName(ref.kind)}/${ref.slug} not found: ${cause instanceof Error ? cause.message : String(cause)}`,
229
- { cause },
234
+ `resource ${kindName(ref.kind)}/${ref.slug} not found${where}`,
230
235
  );
231
236
  }
232
237